diff --git a/README.md b/README.md index 9d270789..e9bbce11 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,6 @@ pre-commit run --all-files pre-commit run pytest-local ``` -Use `./generate-examples.sh` to run install `cloudformation-cli-go-plugin` locally and run `cfn generate` in each example. - Getting started --------------- diff --git a/cfn/cfn.go b/cfn/cfn.go index bb69385b..ace1e2f5 100644 --- a/cfn/cfn.go +++ b/cfn/cfn.go @@ -130,6 +130,7 @@ func makeEventFunc(h Handler) eventFunc { credentials.SessionFromCredentialsProvider(&event.RequestData.CallerCredentials), event.RequestData.PreviousResourceProperties, event.RequestData.ResourceProperties, + event.RequestData.TypeConfiguration, ) p := invoke(handlerFn, request, m, event.Action) r, err := newResponse(&p, event.BearerToken) diff --git a/cfn/event.go b/cfn/event.go index 2fba2f58..44c1ca66 100644 --- a/cfn/event.go +++ b/cfn/event.go @@ -36,6 +36,7 @@ type requestData struct { ProviderLogGroupName string `json:"providerLogGroupName"` StackTags tags `json:"stackTags"` SystemTags tags `json:"systemTags"` + TypeConfiguration json.RawMessage `json:"typeConfiguration"` } // validateEvent ensures the event struct generated from the Lambda SDK is correct diff --git a/cfn/handler/event.go b/cfn/handler/event.go index 9c6ae521..af6cc4bc 100644 --- a/cfn/handler/event.go +++ b/cfn/handler/event.go @@ -34,11 +34,8 @@ type ProgressEvent struct { // and by CREATE/UPDATE/DELETE for final response validation/confirmation ResourceModel interface{} `json:"resourceModel,omitempty"` - // ResourceModels is the output resource instances populated by a LIST for - // synchronous results. ResourceModels must be returned by LIST so it's - // always included in the response. When ResourceModels is not set, null is - // returned. - ResourceModels []interface{} `json:"resourceModels"` + // ResourceModels is the output resource instances populated by a LIST for synchronous results + ResourceModels []interface{} `json:"resourceModels,omitempty"` // NextToken is the token used to request additional pages of resources for a LIST operation NextToken string `json:"nextToken,omitempty"` diff --git a/cfn/handler/event_test.go b/cfn/handler/event_test.go deleted file mode 100644 index 82af0926..00000000 --- a/cfn/handler/event_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package handler - -import ( - "testing" - - "github.com/aws/aws-sdk-go/service/cloudformation" - "github.com/google/go-cmp/cmp" - - "encoding/json" - - "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/encoding" -) - -func TestProgressEventMarshalJSON(t *testing.T) { - type Model struct { - Name *encoding.String - Version *encoding.Float - } - - for _, tt := range []struct { - name string - event ProgressEvent - expected string - }{ - { - name: "not updatable", - event: ProgressEvent{ - Message: "foo", - OperationStatus: Failed, - ResourceModel: Model{ - Name: encoding.NewString("Douglas"), - Version: encoding.NewFloat(42.1), - }, - HandlerErrorCode: cloudformation.HandlerErrorCodeNotUpdatable, - }, - expected: `{"status":"FAILED","errorCode":"NotUpdatable","message":"foo","resourceModel":{"Name":"Douglas","Version":"42.1"},"resourceModels":null}`, - }, - { - name: "list with 1 result", - event: ProgressEvent{ - OperationStatus: Success, - ResourceModels: []interface{}{ - Model{ - Name: encoding.NewString("Douglas"), - Version: encoding.NewFloat(42.1), - }, - }, - }, - expected: `{"status":"SUCCESS","resourceModels":[{"Name":"Douglas","Version":"42.1"}]}`, - }, - { - name: "list with empty array", - event: ProgressEvent{ - OperationStatus: Success, - ResourceModels: []interface{}{}, - }, - expected: `{"status":"SUCCESS","resourceModels":[]}`, - }, - } { - t.Run(tt.name, func(t *testing.T) { - - actual, err := json.Marshal(tt.event) - if err != nil { - t.Errorf("Unexpected error marshaling event JSON: %s", err) - } - - if diff := cmp.Diff(string(actual), tt.expected); diff != "" { - t.Errorf(diff) - } - }) - } - -} diff --git a/cfn/handler/handler_test.go b/cfn/handler/handler_test.go index 597993ef..eed779fd 100644 --- a/cfn/handler/handler_test.go +++ b/cfn/handler/handler_test.go @@ -17,7 +17,7 @@ func TestNewRequest(t *testing.T) { prev := Props{} curr := Props{} - req := NewRequest("foo", nil, rctx, nil, []byte(`{"color": "red"}`), []byte(`{"color": "green"}`)) + req := NewRequest("foo", nil, rctx, nil, []byte(`{"color": "red"}`), []byte(`{"color": "green"}`), []byte(``)) if err := req.UnmarshalPrevious(&prev); err != nil { t.Fatalf("Unable to unmarshal props: %v", err) @@ -43,7 +43,7 @@ func TestNewRequest(t *testing.T) { t.Run("ResourceProps", func(t *testing.T) { t.Run("Invalid Body", func(t *testing.T) { - req := NewRequest("foo", nil, rctx, nil, []byte(``), []byte(``)) + req := NewRequest("foo", nil, rctx, nil, []byte(``), []byte(``), []byte(``)) invalid := struct { Color *int `json:"color"` @@ -61,7 +61,7 @@ func TestNewRequest(t *testing.T) { }) t.Run("Invalid Marshal", func(t *testing.T) { - req := NewRequest("foo", nil, rctx, nil, []byte(`{"color": "ref"}`), []byte(`---BAD JSON---`)) + req := NewRequest("foo", nil, rctx, nil, []byte(`{"color": "ref"}`), []byte(`---BAD JSON---`), []byte(``)) var invalid Props @@ -79,7 +79,7 @@ func TestNewRequest(t *testing.T) { t.Run("PreviousResourceProps", func(t *testing.T) { t.Run("Invalid Marshal", func(t *testing.T) { - req := NewRequest("foo", nil, rctx, nil, []byte(`---BAD JSON---`), []byte(`{"color": "green"}`)) + req := NewRequest("foo", nil, rctx, nil, []byte(`---BAD JSON---`), []byte(`{"color": "green"}`), []byte(``)) var invalid Props diff --git a/cfn/handler/request.go b/cfn/handler/request.go index f44c2cb6..013f5f50 100644 --- a/cfn/handler/request.go +++ b/cfn/handler/request.go @@ -36,6 +36,7 @@ type Request struct { previousResourcePropertiesBody []byte resourcePropertiesBody []byte + typeConfiguration []byte } // RequestContext represents information about the current @@ -61,7 +62,7 @@ type RequestContext struct { } // NewRequest returns a new Request based on the provided parameters -func NewRequest(id string, ctx map[string]interface{}, requestCTX RequestContext, sess *session.Session, previousBody, body []byte) Request { +func NewRequest(id string, ctx map[string]interface{}, requestCTX RequestContext, sess *session.Session, previousBody, body []byte, config []byte) Request { return Request{ LogicalResourceID: id, CallbackContext: ctx, @@ -69,6 +70,7 @@ func NewRequest(id string, ctx map[string]interface{}, requestCTX RequestContext previousResourcePropertiesBody: previousBody, resourcePropertiesBody: body, RequestContext: requestCTX, + typeConfiguration: config, } } @@ -99,3 +101,17 @@ func (r *Request) Unmarshal(v interface{}) error { return nil } + +// UnmarshalType populates the provided interface +// with the current resource type configuration +func (r *Request) UnmarshalType(v interface{}) error { + if len(r.resourcePropertiesBody) == 0 { + return cfnerr.New(bodyEmptyError, "Body is empty", nil) + } + + if err := encoding.Unmarshal(r.typeConfiguration, v); err != nil { + return cfnerr.New(marshalingError, "Unable to convert type", err) + } + + return nil +} diff --git a/cfn/response.go b/cfn/response.go index 1d69fb19..9ff28b5a 100644 --- a/cfn/response.go +++ b/cfn/response.go @@ -31,11 +31,8 @@ type response struct { //passed back to CloudFormation BearerToken string `json:"bearerToken,omitempty"` - // ResourceModels is the output resource instances populated by a LIST for - // synchronous results. ResourceModels must be returned by LIST so it's - // always included in the response. When ResourceModels is not set, null is - // returned. - ResourceModels []interface{} `json:"resourceModels"` + // ResourceModels is the output resource instances populated by a LIST for synchronous results + ResourceModels []interface{} `json:"resourceModels,omitempty"` // NextToken the token used to request additional pages of resources for a LIST operation NextToken string `json:"nextToken,omitempty"` diff --git a/cfn/response_test.go b/cfn/response_test.go index fb296079..f7296b2b 100644 --- a/cfn/response_test.go +++ b/cfn/response_test.go @@ -12,66 +12,31 @@ import ( "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler" ) -func TestResponseMarshalJSON(t *testing.T) { +func TestMarshalJSON(t *testing.T) { type Model struct { Name *encoding.String Version *encoding.Float } - for _, tt := range []struct { - name string - response response - expected string - }{ - { - name: "updated failed", - response: response{ - Message: "foo", - OperationStatus: handler.Failed, - ResourceModel: Model{ - Name: encoding.NewString("Douglas"), - Version: encoding.NewFloat(42.1), - }, - ErrorCode: cloudformation.HandlerErrorCodeNotUpdatable, - BearerToken: "xyzzy", - }, - expected: `{"message":"foo","status":"FAILED","resourceModel":{"Name":"Douglas","Version":"42.1"},"errorCode":"NotUpdatable","bearerToken":"xyzzy","resourceModels":null}`, + r := response{ + Message: "foo", + OperationStatus: handler.Success, + ResourceModel: Model{ + Name: encoding.NewString("Douglas"), + Version: encoding.NewFloat(42.1), }, - { - name: "list with 1 result", - response: response{ - OperationStatus: handler.Success, - ResourceModels: []interface{}{ - Model{ - Name: encoding.NewString("Douglas"), - Version: encoding.NewFloat(42.1), - }, - }, - BearerToken: "xyzzy", - }, - expected: `{"status":"SUCCESS","bearerToken":"xyzzy","resourceModels":[{"Name":"Douglas","Version":"42.1"}]}`, - }, - { - name: "list with empty array", - response: response{ - OperationStatus: handler.Success, - ResourceModels: []interface{}{}, - BearerToken: "xyzzy", - }, - expected: `{"status":"SUCCESS","bearerToken":"xyzzy","resourceModels":[]}`, - }, - } { - t.Run(tt.name, func(t *testing.T) { + ErrorCode: cloudformation.HandlerErrorCodeNotUpdatable, + BearerToken: "xyzzy", + } - actual, err := json.Marshal(tt.response) - if err != nil { - t.Errorf("Unexpected error marshaling response JSON: %s", err) - } + expected := `{"message":"foo","status":"SUCCESS","resourceModel":{"Name":"Douglas","Version":"42.1"},"errorCode":"NotUpdatable","bearerToken":"xyzzy"}` - if diff := cmp.Diff(string(actual), tt.expected); diff != "" { - t.Errorf(diff) - } - }) + actual, err := json.Marshal(r) + if err != nil { + t.Errorf("Unexpected error marshaling response JSON: %s", err) } + if diff := cmp.Diff(string(actual), expected); diff != "" { + t.Errorf(diff) + } } diff --git a/go.mod b/go.mod index 79d072dd..fccbd295 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/aws-cloudformation/cloudformation-cli-go-plugin go 1.13 require ( - github.com/avast/retry-go v2.6.0+incompatible github.com/aws/aws-lambda-go v1.13.3 github.com/aws/aws-sdk-go v1.25.37 github.com/google/go-cmp v0.3.1 diff --git a/go.sum b/go.sum index cf869761..d4b8057c 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/avast/retry-go v2.6.0+incompatible h1:FelcMrm7Bxacr1/RM8+/eqkDkmVN7tjlsy51dOzB3LI= -github.com/avast/retry-go v2.6.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/aws/aws-lambda-go v1.13.3 h1:SuCy7H3NLyp+1Mrfp+m80jcbi9KYWAs9/BXwppwRDzY= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.25.37 h1:gBtB/F3dophWpsUQKN/Kni+JzYEH2mGHF4hWNtfED1w= @@ -9,7 +7,6 @@ github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpi github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -38,7 +35,6 @@ golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BG golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/validator.v2 v2.0.0-20191107172027-c3144fdedc21 h1:2QQcyaEBdpfjjYkF0MXc69jZbHb4IOYuXz2UwsmVM8k= diff --git a/python/rpdk/go/codegen.py b/python/rpdk/go/codegen.py index 8e945354..2d0b7c4b 100644 --- a/python/rpdk/go/codegen.py +++ b/python/rpdk/go/codegen.py @@ -11,6 +11,7 @@ from rpdk.core.init import input_with_validation from rpdk.core.jsonutils.resolver import resolve_models from rpdk.core.plugin_base import LanguagePlugin +from rpdk.core.project import Project from . import __version__ from .resolver import translate_type @@ -47,9 +48,7 @@ def __init__(self): def _prompt_for_go_path(self, project): path_validator = validate_path("") - import_path = path_validator( - project.settings.get("import_path", project.settings.get("importpath")) - ) + import_path = path_validator(project.settings.get("import_path")) if not import_path: prompt = "Enter the GO Import path" @@ -58,7 +57,7 @@ def _prompt_for_go_path(self, project): self.import_path = import_path project.settings["import_path"] = str(self.import_path) - def init(self, project): + def init(self, project: Project): LOG.debug("Init started") self._prompt_for_go_path(project) @@ -90,11 +89,7 @@ def init(self, project): path = project.root / "go.mod" LOG.debug("Writing go.mod: %s", path) template = self.env.get_template("go.mod.tple") - contents = template.render( - path=Path( - project.settings.get("import_path", project.settings.get("importpath")) - ) - ) + contents = template.render(path=Path(project.settings["import_path"])) project.safewrite(path, contents) # CloudFormation/SAM template for handler lambda @@ -140,23 +135,23 @@ def init(self, project): LOG.debug("Init complete") - def _init_settings(self, project): + def _init_settings(self, project: Project): project.runtime = self.RUNTIME project.entrypoint = self.ENTRY_POINT.format(self.import_path) project.test_entrypoint = self.TEST_ENTRY_POINT.format(self.import_path) project.settings.update(DEFAULT_SETTINGS) - def init_handlers(self, project, src): + def init_handlers(self, project: Project, src): LOG.debug("Writing stub handlers") template = self.env.get_template("stubHandler.go.tple") path = src / "resource.go" contents = template.render() project.safewrite(path, contents) - def _get_generated_root(self, project): + def _get_generated_root(self, project: Project): LOG.debug("Init started") - def generate(self, project): + def generate(self, project: Project): LOG.debug("Generate started") root = project.root / "cmd" @@ -165,7 +160,27 @@ def generate(self, project): format_paths = [] LOG.debug("Writing Types") + models = resolve_models(project.schema) + if project.configuration_schema: + configuration_schema_path = ( + project.root / project.configuration_schema_filename + ) + project.write_configuration_schema(configuration_schema_path) + configuration_models = resolve_models( + project.configuration_schema, "TypeConfiguration" + ) + else: + configuration_models = {"TypeConfiguration": {}} + + # Create the type configuration model + template = self.env.get_template("config.go.tple") + path = src / "{}.go".format("config") + contents = template.render(models=configuration_models) + project.overwrite(path, contents) + format_paths.append(path) + + # Create the resource model template = self.env.get_template("types.go.tple") path = src / "{}.go".format("model") contents = template.render(models=models) @@ -175,9 +190,7 @@ def generate(self, project): path = root / "main.go" LOG.debug("Writing project: %s", path) template = self.env.get_template("main.go.tple") - importpath = Path( - project.settings.get("import_path", project.settings.get("importpath")) - ) + importpath = Path(project.settings["import_path"]) contents = template.render(path=(importpath / "cmd" / "resource").as_posix()) project.overwrite(path, contents) format_paths.append(path) @@ -216,7 +229,7 @@ def generate(self, project): project.write_settings() @staticmethod - def pre_package(project): + def pre_package(project: Project): # zip the Go build output - it's all needed to execute correctly f = TemporaryFile("w+b") @@ -229,7 +242,7 @@ def pre_package(project): return f @staticmethod - def _find_exe(project): + def _find_exe(project: Project): exe_glob = list((project.root / "bin").glob("{}".format("handler"))) if not exe_glob: LOG.debug("No Go executable match") @@ -250,7 +263,7 @@ def _find_exe(project): LOG.debug("Generate complete") - def package(self, project, zip_file): + def package(self, project: Project, zip_file): LOG.info("Packaging Go project") def write_with_relative_path(path): @@ -272,27 +285,3 @@ def write_with_relative_path(path): for path in (project.root / "internal").rglob("*"): if path.is_file(): write_with_relative_path(path) - - @staticmethod - def _get_plugin_information(project): - module_file = project.root / "go.mod" - plugin_version = None - - with open(module_file) as f: - line = f.readline() - while line: - if "github.com/aws-cloudformation/cloudformation-cli-go-plugin" in line: - plugin_version = line.strip().split(" ")[-1] - break - else: - line = f.readline() - - plugin_info = {"plugin-tool-version": __version__, "plugin-name": "go"} - - if plugin_version is not None: - plugin_info["plugin-version"] = plugin_version - - return plugin_info - - def get_plugin_information(self, project): - return self._get_plugin_information(project) diff --git a/python/rpdk/go/templates/config.go.tple b/python/rpdk/go/templates/config.go.tple new file mode 100644 index 00000000..2d18e4dc --- /dev/null +++ b/python/rpdk/go/templates/config.go.tple @@ -0,0 +1,29 @@ +// Code generated by 'cfn generate', changes will be undone by the next invocation. DO NOT EDIT. +// Updates to this type are made my editing the schema file and executing the 'generate' command. +package resource +import "github.com/aws-cloudformation/cloudformation-cli-go-plugin/cfn/handler" + +{% for model_name, properties in models.items() %} + +{% if model_name == "ResourceModel" %} + {% set model_name = "Model" %} +{% else %} + {% set model_name = model_name | uppercase_first_letter %} +{% endif %} +// {{model_name}} is autogenerated from the json schema +type {{model_name}} struct { +{% for name, type in properties.items() %} + {{ name|uppercase_first_letter }} {{ type|translate_type }} `json:",omitempty"` +{% endfor %} +} + +{% endfor %} + +// Configuration returns a resource's configuration. +func Configuration(req handler.Request) (TypeConfiguration, error) { + t := &TypeConfiguration{} + if err := req.UnmarshalType(t); err != nil { + return *t, err + } + return *t, nil +} diff --git a/python/rpdk/go/templates/stubHandler.go.tple b/python/rpdk/go/templates/stubHandler.go.tple index d390dad8..089ab3c4 100644 --- a/python/rpdk/go/templates/stubHandler.go.tple +++ b/python/rpdk/go/templates/stubHandler.go.tple @@ -14,6 +14,7 @@ func {{ method }}(req handler.Request, prevModel *Model, currentModel *Model) (h // * Make API calls (use req.Session) // * Mutate the model // * Check/set any callback context (req.CallbackContext / response.CallbackContext) + // * Access the resource's configuration with the Configuration function. (c, err := Configuration(req)) /* // Construct a new handler.ProgressEvent and return it diff --git a/tests/data/schema-with-typeconfiguration.json b/tests/data/schema-with-typeconfiguration.json new file mode 100644 index 00000000..ef3c15b5 --- /dev/null +++ b/tests/data/schema-with-typeconfiguration.json @@ -0,0 +1,50 @@ +{ + "typeName": "Company::Test::Type", + "description": "Test type", + "typeConfiguration": { + "properties": { + "Credentials": { + "$ref": "#/definitions/Credentials" + } + }, + "additionalProperties": false, + "required": [ + "Credentials" + ] + }, + "definitions": { + "Credentials": { + "type": "object", + "properties": { + "ApiKey": { + "description": "API key", + "type": "string" + }, + "ApplicationKey": { + "description": "application key", + "type": "string" + }, + "CountryCode": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "properties": { + "Type": { + "type": "string", + "description": "The type of the monitor", + "enum": [ + "composite" + ] + } + }, + "required": [ + "Type" + ], + "primaryIdentifier": [ + "/properties/Type" + ], + "additionalProperties": false +} diff --git a/tests/plugin/_init_.py b/tests/plugin/_init_.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/plugin/codegen_test.py b/tests/plugin/codegen_test.py new file mode 100644 index 00000000..5a1d247e --- /dev/null +++ b/tests/plugin/codegen_test.py @@ -0,0 +1,130 @@ +# pylint: disable=redefined-outer-name,protected-access +import ast +import importlib.util +from pathlib import Path +from shutil import copyfile +from subprocess import CalledProcessError +from unittest.mock import ANY, patch, sentinel +from uuid import uuid4 +from zipfile import ZipFile + +import pytest +from docker.errors import APIError, ContainerError, ImageLoadError +from requests.exceptions import ConnectionError as RequestsConnectionError +from rpdk.core.exceptions import DownstreamError +from rpdk.core.project import Project +from rpdk.go.codegen import GoLanguagePlugin + +TYPE_NAME = "foo::bar::baz" + + +@pytest.fixture +def plugin(): + return GoLanguagePlugin() + + +@pytest.fixture +def project(tmp_path): + project = Project(root=tmp_path) + patch_plugins = patch.dict( + "rpdk.core.plugin_registry.PLUGIN_REGISTRY", + {GoLanguagePlugin.RUNTIME: lambda: GoLanguagePlugin}, + clear=True, + ) + patch_wizard = patch( + "rpdk.go.codegen.input_with_validation", autospec=True, side_effect=[False] + ) + with patch_plugins, patch_wizard: + project.init(TYPE_NAME, GoLanguagePlugin.RUNTIME) + return project + + +def get_files_in_project(project): + return { + str(child.relative_to(project.root)): child for child in project.root.rglob("*") + } + + +def test_initialize(project): + print(project.settings) + assert project.settings == { + "import_path": "False", + "protocolVersion": "2.0.0", + "pluginVersion": "2.0.1", + } + files = get_files_in_project(project) + assert set(files) == { + ".gitignore", + ".rpdk-config", + "README.md", + "foo-bar-baz.json", + "example_inputs/inputs_1_invalid.json", + "example_inputs/inputs_1_update.json", + "example_inputs/inputs_1_create.json", + "example_inputs", + "cmd", + "cmd/resource/resource.go", + "template.yml", + "cmd/resource", + "go.mod", + "internal", + "Makefile", + } + + +def test_generate(project): + project.load_schema() + before = get_files_in_project(project) + project.generate() + after = get_files_in_project(project) + files = ( + after.keys() + - before.keys() + - {"resource-role.yaml", "cmd/main.go", "makebuild"} + ) + assert files == {"cmd/resource/model.go", "go.sum"} + type_configuration_schema_file = project.root / "foo-bar-baz-configuration.json" + assert not type_configuration_schema_file.is_file() + + +def test_generate_with_type_configuration(tmp_path): + type_name = "schema::with::typeconfiguration" + project = Project(root=tmp_path) + + patch_plugins = patch.dict( + "rpdk.core.plugin_registry.PLUGIN_REGISTRY", + {GoLanguagePlugin.RUNTIME: lambda: GoLanguagePlugin}, + clear=True, + ) + patch_wizard = patch( + "rpdk.go.codegen.input_with_validation", autospec=True, side_effect=[False] + ) + with patch_plugins, patch_wizard: + project.init(type_name, GoLanguagePlugin.RUNTIME) + + copyfile( + str(Path.cwd() / "data/schema-with-typeconfiguration.json"), + str(project.root / "schema-with-typeconfiguration.json"), + ) + project.type_info = ("schema", "with", "typeconfiguration") + project.load_schema() + project.load_configuration_schema() + project.generate() + + # assert TypeConfigurationModel is added to generated directory + # models_path = project.root / "src" / "schema_with_typeconfiguration" / "models.py" + + # this however loads the module + # spec = importlib.util.spec_from_file_location("foo_bar_baz.models", models_path) + # module = importlib.util.module_from_spec(spec) + # spec.loader.exec_module(module) + + # assert hasattr(module.ResourceModel, "_serialize") + # assert hasattr(module.ResourceModel, "_deserialize") + # assert hasattr(module.TypeConfigurationModel, "_serialize") + # assert hasattr(module.TypeConfigurationModel, "_deserialize") + + type_configuration_schema_file = ( + project.root / "schema-with-typeconfiguration-configuration.json" + ) + assert type_configuration_schema_file.is_file()