diff --git a/CI/circle_parallel.sh b/CI/circle_parallel.sh index 183c77c8c624..1d96dae7b13e 100755 --- a/CI/circle_parallel.sh +++ b/CI/circle_parallel.sh @@ -80,6 +80,7 @@ elif [ "$NODE_INDEX" = "4" ]; then #mvn --no-snapshot-updates --quiet verify -Psamples.circleci.node4 -Dorg.slf4j.simpleLogger.defaultLogLevel=error (cd samples/openapi3/client/petstore/python && make test) (cd samples/openapi3/client/petstore/python-experimental && make test) + (cd samples/openapi3/client/3_0_3_unit_test/python-experimental && make test) else echo "Running node $NODE_INDEX to test 'samples.circleci.others' defined in pom.xml ..." diff --git a/bin/configs/python-experimental_3_0_3_unit_test.yaml b/bin/configs/python-experimental_3_0_3_unit_test.yaml new file mode 100644 index 000000000000..3169f0804e1a --- /dev/null +++ b/bin/configs/python-experimental_3_0_3_unit_test.yaml @@ -0,0 +1,6 @@ +generatorName: python-experimental +outputDir: samples/openapi3/client/3_0_3_unit_test/python-experimental +inputSpec: modules/openapi-generator/src/test/resources/3_0/unit_test_spec/type.yaml +templateDir: modules/openapi-generator/src/main/resources/python-experimental +additionalProperties: + packageName: unit_test_api diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index b20c11311144..2c9d261162b4 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -2696,6 +2696,35 @@ protected void updateModelForAnyType(CodegenModel m, Schema schema) { setAddProps(schema, m); } + protected String toTesCaseName(String specTestCaseName) { + return specTestCaseName; + } + + /** + * Processes any test cases if they exist in the vendor extensions + * If they exist then cast them to java class instances and assign them back as the value + * in the x-test-examples vendor extension + * @param vendorExtensions + */ + private void processTestCases(HashMap vendorExtensions) { + String testExamplesKey = "x-test-examples"; + HashMap testCases = new HashMap<>(); + if (vendorExtensions.containsKey(testExamplesKey)) { + LinkedHashMap testExamples = (LinkedHashMap) vendorExtensions.get(testExamplesKey); + for(Map.Entry testExampleEntry: testExamples.entrySet()) { + String exampleName = testExampleEntry.getKey(); + LinkedHashMap testExample = (LinkedHashMap) testExampleEntry.getValue(); + String nameInSnakeCase = toTesCaseName(exampleName); + SchemaTestCase testCase = new SchemaTestCase( + (String) testExample.getOrDefault("description", ""), + new ObjectWithTypeBooleans(testExample.get("data")), + (boolean) testExample.get("valid") + ); + testCases.put(nameInSnakeCase, testCase); + } + vendorExtensions.put(testExamplesKey, testCases); + } + } /** * Convert OAS Model object to Codegen Model object. @@ -2706,6 +2735,9 @@ protected void updateModelForAnyType(CodegenModel m, Schema schema) { */ @Override public CodegenModel fromModel(String name, Schema schema) { + HashMap vendorExtensions = (HashMap) schema.getExtensions(); + processTestCases(vendorExtensions); + Map allDefinitions = ModelUtils.getSchemas(this.openAPI); if (typeAliases == null) { // Only do this once during first call diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/ObjectWithTypeBooleans.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/ObjectWithTypeBooleans.java new file mode 100644 index 000000000000..7f450b51c2fc --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/ObjectWithTypeBooleans.java @@ -0,0 +1,54 @@ +package org.openapitools.codegen; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.Map; + +public class ObjectWithTypeBooleans { + public boolean isUnboundedInteger; + public boolean isNumber; + public boolean isString; + public boolean isMap; + public boolean isArray; + public boolean isBoolean; + public boolean isNull; + public Object value; + + public ObjectWithTypeBooleans(Object value) { + Object usedValue = null; + if (value instanceof Integer){ + this.isUnboundedInteger = true; + this.value = value; + } else if (value instanceof Double || value instanceof Float){ + this.isNumber = true; + this.value = value; + } else if (value instanceof String) { + this.isString = true; + this.value = value; + } else if (value instanceof LinkedHashMap) { + LinkedHashMap castValue = (LinkedHashMap) value; + LinkedHashMap castMap = new LinkedHashMap<>(); + for (Map.Entry entry: castValue.entrySet()) { + String entryKey = (String) entry.getKey(); + ObjectWithTypeBooleans entryValue = new ObjectWithTypeBooleans(entry.getValue()); + castMap.put(entryKey, entryValue); + } + this.value = castMap; + this.isMap = true; + } else if (value instanceof ArrayList) { + ArrayList castList = new ArrayList<>(); + for (Object item: (ArrayList) value) { + castList.add(new ObjectWithTypeBooleans(item)); + } + this.value = castList; + this.isArray = true; + } else if (value instanceof Boolean) { + this.isBoolean = true; + this.value = value; + } else if (value == null) { + this.isNull = true; + this.value = value; + } + } +} + diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/SchemaTestCase.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/SchemaTestCase.java new file mode 100644 index 000000000000..c377fe704e2f --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/SchemaTestCase.java @@ -0,0 +1,14 @@ +package org.openapitools.codegen; + +public class SchemaTestCase { + public String description; + public ObjectWithTypeBooleans data; + // true means the test case should pass, false means it should fail + public boolean valid; + + public SchemaTestCase(String description, ObjectWithTypeBooleans data, boolean valid) { + this.description = description; + this.data = data; + this.valid = valid; + } +} diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonExperimentalClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonExperimentalClientCodegen.java index 8657494678a4..7a2a9099e23b 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonExperimentalClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/PythonExperimentalClientCodegen.java @@ -19,6 +19,7 @@ import com.github.curiousoddman.rgxgen.RgxGen; import com.github.curiousoddman.rgxgen.config.RgxGenOption; import com.github.curiousoddman.rgxgen.config.RgxGenProperties; +import com.google.common.base.CaseFormat; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.Operation; import io.swagger.v3.oas.models.servers.Server; @@ -1152,6 +1153,10 @@ protected void addParentContainer(CodegenModel model, String name, Schema schema model.dataType = getTypeString(schema, "", "", referencedModelNames); } + protected String toTesCaseName(String specTestCaseName) { + return CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, specTestCaseName); + } + /** * Convert OAS Model object to Codegen Model object * We have a custom version of this method so we can: diff --git a/modules/openapi-generator/src/main/resources/python-experimental/model_templates/payload_renderer.handlebars b/modules/openapi-generator/src/main/resources/python-experimental/model_templates/payload_renderer.handlebars new file mode 100644 index 000000000000..12d9c8c77bcf --- /dev/null +++ b/modules/openapi-generator/src/main/resources/python-experimental/model_templates/payload_renderer.handlebars @@ -0,0 +1,33 @@ +{{#or isNumber isUnboundedInteger}} + {{value}} +{{/or}} +{{#if isBoolean}} + {{#if value}} + True + {{else}} + False + {{/if}} +{{/if}} +{{#if isNull}} + None +{{/if}} +{{#if isString}} + "{{value}}" +{{/if}} +{{#if isArray}} + [ + {{#each value}} + {{> model_templates/payload_renderer }} + {{/each}} + ] +{{/if}} +{{#if isMap}} + { + {{#each value}} + "{{@key}}": + {{#with this}} + {{> model_templates/payload_renderer }}, + {{/with}} + {{/each}} + } +{{/if}} \ No newline at end of file diff --git a/modules/openapi-generator/src/main/resources/python-experimental/model_test.handlebars b/modules/openapi-generator/src/main/resources/python-experimental/model_test.handlebars index 48a4a7c85a12..4f4ab258434d 100644 --- a/modules/openapi-generator/src/main/resources/python-experimental/model_test.handlebars +++ b/modules/openapi-generator/src/main/resources/python-experimental/model_test.handlebars @@ -13,18 +13,29 @@ from {{packageName}}.{{modelPackage}}.{{classFilename}} import {{classname}} class Test{{classname}}(unittest.TestCase): """{{classname}} unit test stubs""" - def setUp(self): - pass - - def tearDown(self): - pass - - def test_{{classname}}(self): - """Test {{classname}}""" - # FIXME: construct object with mandatory attributes with example values - # model = {{classname}}() # noqa: E501 - pass +{{#each vendorExtensions}} +{{#if @key eq "x-test-examples" }} +{{#each this }} + def test_{{@key}}_{{#if valid}}passes{{else}}fails{{/if}}(self): + # {{description}} +{{#if valid}} + {{classname}}( +{{#with data}} + {{>model_templates/payload_renderer}} +{{/with}} + ) +{{else}} + with self.assertRaises(({{packageName}}.ApiValueError, {{packageName}}.ApiTypeError)): + {{classname}}( +{{#with data}} + {{>model_templates/payload_renderer}} +{{/with}} + ) +{{/if}} +{{/each}} +{{/if}} +{{/each}} {{/with}} {{/each}} diff --git a/modules/openapi-generator/src/main/resources/python-experimental/schemas.handlebars b/modules/openapi-generator/src/main/resources/python-experimental/schemas.handlebars index f9a69ff2b751..d7eb0accba28 100644 --- a/modules/openapi-generator/src/main/resources/python-experimental/schemas.handlebars +++ b/modules/openapi-generator/src/main/resources/python-experimental/schemas.handlebars @@ -1865,8 +1865,9 @@ class IntBase(NumberBase): @classmethod def _validate_format(cls, arg: typing.Optional[decimal.Decimal], validation_metadata: ValidationMetadata): if isinstance(arg, decimal.Decimal): - exponent = arg.as_tuple().exponent - if exponent != 0: + + denominator = arg.as_integer_ratio()[-1] + if denominator != 1: raise ApiValueError( "Invalid value '{}' for type integer at {}".format(arg, validation_metadata.path_to_item) ) diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/.gitignore b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/.gitignore new file mode 100644 index 000000000000..f5e96dbfaec8 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/.gitignore @@ -0,0 +1 @@ +venv \ No newline at end of file diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/additionalItems.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/additionalItems.json new file mode 100755 index 000000000000..784bc8461058 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/additionalItems.json @@ -0,0 +1,149 @@ +[ + { + "description": "additionalItems as schema", + "schema": { + "items": [{}], + "additionalItems": {"type": "integer"} + }, + "tests": [ + { + "description": "additional items match schema", + "data": [ null, 2, 3, 4 ], + "valid": true + }, + { + "description": "additional items do not match schema", + "data": [ null, 2, 3, "foo" ], + "valid": false + } + ] + }, + { + "description": "when items is schema, additionalItems does nothing", + "schema": { + "items": {}, + "additionalItems": false + }, + "tests": [ + { + "description": "all items match schema", + "data": [ 1, 2, 3, 4, 5 ], + "valid": true + } + ] + }, + { + "description": "array of items with no additionalItems permitted", + "schema": { + "items": [{}, {}, {}], + "additionalItems": false + }, + "tests": [ + { + "description": "empty array", + "data": [ ], + "valid": true + }, + { + "description": "fewer number of items present (1)", + "data": [ 1 ], + "valid": true + }, + { + "description": "fewer number of items present (2)", + "data": [ 1, 2 ], + "valid": true + }, + { + "description": "equal number of items present", + "data": [ 1, 2, 3 ], + "valid": true + }, + { + "description": "additional items are not permitted", + "data": [ 1, 2, 3, 4 ], + "valid": false + } + ] + }, + { + "description": "additionalItems as false without items", + "schema": {"additionalItems": false}, + "tests": [ + { + "description": + "items defaults to empty schema so everything is valid", + "data": [ 1, 2, 3, 4, 5 ], + "valid": true + }, + { + "description": "ignores non-arrays", + "data": {"foo" : "bar"}, + "valid": true + } + ] + }, + { + "description": "additionalItems are allowed by default", + "schema": {"items": [{"type": "integer"}]}, + "tests": [ + { + "description": "only the first item is validated", + "data": [1, "foo", false], + "valid": true + } + ] + }, + { + "description": "additionalItems should not look in applicators, valid case", + "schema": { + "allOf": [ + { "items": [ { "type": "integer" } ] } + ], + "additionalItems": { "type": "boolean" } + }, + "tests": [ + { + "description": "items defined in allOf are not examined", + "data": [ 1, null ], + "valid": true + } + ] + }, + { + "description": "additionalItems should not look in applicators, invalid case", + "schema": { + "allOf": [ + { "items": [ { "type": "integer" }, { "type": "string" } ] } + ], + "items": [ {"type": "integer" } ], + "additionalItems": { "type": "boolean" } + }, + "tests": [ + { + "description": "items defined in allOf are not examined", + "data": [ 1, "hello" ], + "valid": false + } + ] + }, + { + "description": "items validation adjusts the starting index for additionalItems", + "schema": { + "items": [ { "type": "string" } ], + "additionalItems": { "type": "integer" } + }, + "tests": [ + { + "description": "valid items", + "data": [ "x", 2, 3 ], + "valid": true + }, + { + "description": "wrong type of second item", + "data": [ "x", "y" ], + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/additionalProperties.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/additionalProperties.json new file mode 100755 index 000000000000..381275a596ad --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/additionalProperties.json @@ -0,0 +1,133 @@ +[ + { + "description": + "additionalProperties being false does not allow other properties", + "schema": { + "properties": {"foo": {}, "bar": {}}, + "patternProperties": { "^v": {} }, + "additionalProperties": false + }, + "tests": [ + { + "description": "no additional properties is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "an additional property is invalid", + "data": {"foo" : 1, "bar" : 2, "quux" : "boom"}, + "valid": false + }, + { + "description": "ignores arrays", + "data": [1, 2, 3], + "valid": true + }, + { + "description": "ignores strings", + "data": "foobarbaz", + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + }, + { + "description": "patternProperties are not additional properties", + "data": {"foo":1, "vroom": 2}, + "valid": true + } + ] + }, + { + "description": "non-ASCII pattern with additionalProperties", + "schema": { + "patternProperties": {"^á": {}}, + "additionalProperties": false + }, + "tests": [ + { + "description": "matching the pattern is valid", + "data": {"ármányos": 2}, + "valid": true + }, + { + "description": "not matching the pattern is invalid", + "data": {"élmény": 2}, + "valid": false + } + ] + }, + { + "description": + "additionalProperties allows a schema which should validate", + "schema": { + "properties": {"foo": {}, "bar": {}}, + "additionalProperties": {"type": "boolean"} + }, + "tests": [ + { + "description": "no additional properties is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "an additional valid property is valid", + "data": {"foo" : 1, "bar" : 2, "quux" : true}, + "valid": true + }, + { + "description": "an additional invalid property is invalid", + "data": {"foo" : 1, "bar" : 2, "quux" : 12}, + "valid": false + } + ] + }, + { + "description": + "additionalProperties can exist by itself", + "schema": { + "additionalProperties": {"type": "boolean"} + }, + "tests": [ + { + "description": "an additional valid property is valid", + "data": {"foo" : true}, + "valid": true + }, + { + "description": "an additional invalid property is invalid", + "data": {"foo" : 1}, + "valid": false + } + ] + }, + { + "description": "additionalProperties are allowed by default", + "schema": {"properties": {"foo": {}, "bar": {}}}, + "tests": [ + { + "description": "additional properties are allowed", + "data": {"foo": 1, "bar": 2, "quux": true}, + "valid": true + } + ] + }, + { + "description": "additionalProperties should not look in applicators", + "schema": { + "allOf": [ + {"properties": {"foo": {}}} + ], + "additionalProperties": {"type": "boolean"} + }, + "tests": [ + { + "description": "properties defined in allOf are not examined", + "data": {"foo": 1, "bar": true}, + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/allOf.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/allOf.json new file mode 100755 index 000000000000..ec9319e1436f --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/allOf.json @@ -0,0 +1,294 @@ +[ + { + "description": "allOf", + "schema": { + "allOf": [ + { + "properties": { + "bar": {"type": "integer"} + }, + "required": ["bar"] + }, + { + "properties": { + "foo": {"type": "string"} + }, + "required": ["foo"] + } + ] + }, + "tests": [ + { + "description": "allOf", + "data": {"foo": "baz", "bar": 2}, + "valid": true + }, + { + "description": "mismatch second", + "data": {"foo": "baz"}, + "valid": false + }, + { + "description": "mismatch first", + "data": {"bar": 2}, + "valid": false + }, + { + "description": "wrong type", + "data": {"foo": "baz", "bar": "quux"}, + "valid": false + } + ] + }, + { + "description": "allOf with base schema", + "schema": { + "properties": {"bar": {"type": "integer"}}, + "required": ["bar"], + "allOf" : [ + { + "properties": { + "foo": {"type": "string"} + }, + "required": ["foo"] + }, + { + "properties": { + "baz": {"type": "null"} + }, + "required": ["baz"] + } + ] + }, + "tests": [ + { + "description": "valid", + "data": {"foo": "quux", "bar": 2, "baz": null}, + "valid": true + }, + { + "description": "mismatch base schema", + "data": {"foo": "quux", "baz": null}, + "valid": false + }, + { + "description": "mismatch first allOf", + "data": {"bar": 2, "baz": null}, + "valid": false + }, + { + "description": "mismatch second allOf", + "data": {"foo": "quux", "bar": 2}, + "valid": false + }, + { + "description": "mismatch both", + "data": {"bar": 2}, + "valid": false + } + ] + }, + { + "description": "allOf simple types", + "schema": { + "allOf": [ + {"maximum": 30}, + {"minimum": 20} + ] + }, + "tests": [ + { + "description": "valid", + "data": 25, + "valid": true + }, + { + "description": "mismatch one", + "data": 35, + "valid": false + } + ] + }, + { + "description": "allOf with boolean schemas, all true", + "schema": {"allOf": [true, true]}, + "tests": [ + { + "description": "any value is valid", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "allOf with boolean schemas, some false", + "schema": {"allOf": [true, false]}, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "allOf with boolean schemas, all false", + "schema": {"allOf": [false, false]}, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "allOf with one empty schema", + "schema": { + "allOf": [ + {} + ] + }, + "tests": [ + { + "description": "any data is valid", + "data": 1, + "valid": true + } + ] + }, + { + "description": "allOf with two empty schemas", + "schema": { + "allOf": [ + {}, + {} + ] + }, + "tests": [ + { + "description": "any data is valid", + "data": 1, + "valid": true + } + ] + }, + { + "description": "allOf with the first empty schema", + "schema": { + "allOf": [ + {}, + { "type": "number" } + ] + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "string is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "allOf with the last empty schema", + "schema": { + "allOf": [ + { "type": "number" }, + {} + ] + }, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "string is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "nested allOf, to check validation semantics", + "schema": { + "allOf": [ + { + "allOf": [ + { + "type": "null" + } + ] + } + ] + }, + "tests": [ + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "anything non-null is invalid", + "data": 123, + "valid": false + } + ] + }, + { + "description": "allOf combined with anyOf, oneOf", + "schema": { + "allOf": [ { "multipleOf": 2 } ], + "anyOf": [ { "multipleOf": 3 } ], + "oneOf": [ { "multipleOf": 5 } ] + }, + "tests": [ + { + "description": "allOf: false, anyOf: false, oneOf: false", + "data": 1, + "valid": false + }, + { + "description": "allOf: false, anyOf: false, oneOf: true", + "data": 5, + "valid": false + }, + { + "description": "allOf: false, anyOf: true, oneOf: false", + "data": 3, + "valid": false + }, + { + "description": "allOf: false, anyOf: true, oneOf: true", + "data": 15, + "valid": false + }, + { + "description": "allOf: true, anyOf: false, oneOf: false", + "data": 2, + "valid": false + }, + { + "description": "allOf: true, anyOf: false, oneOf: true", + "data": 10, + "valid": false + }, + { + "description": "allOf: true, anyOf: true, oneOf: false", + "data": 6, + "valid": false + }, + { + "description": "allOf: true, anyOf: true, oneOf: true", + "data": 30, + "valid": true + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/anyOf.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/anyOf.json new file mode 100755 index 000000000000..b720afa8de38 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/anyOf.json @@ -0,0 +1,215 @@ +[ + { + "description": "anyOf", + "schema": { + "anyOf": [ + { + "type": "integer" + }, + { + "minimum": 2 + } + ] + }, + "tests": [ + { + "description": "first anyOf valid", + "data": 1, + "valid": true + }, + { + "description": "second anyOf valid", + "data": 2.5, + "valid": true + }, + { + "description": "both anyOf valid", + "data": 3, + "valid": true + }, + { + "description": "neither anyOf valid", + "data": 1.5, + "valid": false + } + ] + }, + { + "description": "anyOf with base schema", + "schema": { + "type": "string", + "anyOf" : [ + { + "maxLength": 2 + }, + { + "minLength": 4 + } + ] + }, + "tests": [ + { + "description": "mismatch base schema", + "data": 3, + "valid": false + }, + { + "description": "one anyOf valid", + "data": "foobar", + "valid": true + }, + { + "description": "both anyOf invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "anyOf with boolean schemas, all true", + "schema": {"anyOf": [true, true]}, + "tests": [ + { + "description": "any value is valid", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "anyOf with boolean schemas, some true", + "schema": {"anyOf": [true, false]}, + "tests": [ + { + "description": "any value is valid", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "anyOf with boolean schemas, all false", + "schema": {"anyOf": [false, false]}, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "anyOf complex types", + "schema": { + "anyOf": [ + { + "properties": { + "bar": {"type": "integer"} + }, + "required": ["bar"] + }, + { + "properties": { + "foo": {"type": "string"} + }, + "required": ["foo"] + } + ] + }, + "tests": [ + { + "description": "first anyOf valid (complex)", + "data": {"bar": 2}, + "valid": true + }, + { + "description": "second anyOf valid (complex)", + "data": {"foo": "baz"}, + "valid": true + }, + { + "description": "both anyOf valid (complex)", + "data": {"foo": "baz", "bar": 2}, + "valid": true + }, + { + "description": "neither anyOf valid (complex)", + "data": {"foo": 2, "bar": "quux"}, + "valid": false + } + ] + }, + { + "description": "anyOf with one empty schema", + "schema": { + "anyOf": [ + { "type": "number" }, + {} + ] + }, + "tests": [ + { + "description": "string is valid", + "data": "foo", + "valid": true + }, + { + "description": "number is valid", + "data": 123, + "valid": true + } + ] + }, + { + "description": "nested anyOf, to check validation semantics", + "schema": { + "anyOf": [ + { + "anyOf": [ + { + "type": "null" + } + ] + } + ] + }, + "tests": [ + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "anything non-null is invalid", + "data": 123, + "valid": false + } + ] + }, + { + "description": "nested anyOf, to check validation semantics", + "schema": { + "anyOf": [ + { + "anyOf": [ + { + "type": "null" + } + ] + } + ] + }, + "tests": [ + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "anything non-null is invalid", + "data": 123, + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/boolean_schema.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/boolean_schema.json new file mode 100755 index 000000000000..6d40f23f2622 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/boolean_schema.json @@ -0,0 +1,104 @@ +[ + { + "description": "boolean schema 'true'", + "schema": true, + "tests": [ + { + "description": "number is valid", + "data": 1, + "valid": true + }, + { + "description": "string is valid", + "data": "foo", + "valid": true + }, + { + "description": "boolean true is valid", + "data": true, + "valid": true + }, + { + "description": "boolean false is valid", + "data": false, + "valid": true + }, + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "object is valid", + "data": {"foo": "bar"}, + "valid": true + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + }, + { + "description": "array is valid", + "data": ["foo"], + "valid": true + }, + { + "description": "empty array is valid", + "data": [], + "valid": true + } + ] + }, + { + "description": "boolean schema 'false'", + "schema": false, + "tests": [ + { + "description": "number is invalid", + "data": 1, + "valid": false + }, + { + "description": "string is invalid", + "data": "foo", + "valid": false + }, + { + "description": "boolean true is invalid", + "data": true, + "valid": false + }, + { + "description": "boolean false is invalid", + "data": false, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + }, + { + "description": "object is invalid", + "data": {"foo": "bar"}, + "valid": false + }, + { + "description": "empty object is invalid", + "data": {}, + "valid": false + }, + { + "description": "array is invalid", + "data": ["foo"], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/const.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/const.json new file mode 100755 index 000000000000..1c2cafcc19b0 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/const.json @@ -0,0 +1,342 @@ +[ + { + "description": "const validation", + "schema": {"const": 2}, + "tests": [ + { + "description": "same value is valid", + "data": 2, + "valid": true + }, + { + "description": "another value is invalid", + "data": 5, + "valid": false + }, + { + "description": "another type is invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "const with object", + "schema": {"const": {"foo": "bar", "baz": "bax"}}, + "tests": [ + { + "description": "same object is valid", + "data": {"foo": "bar", "baz": "bax"}, + "valid": true + }, + { + "description": "same object with different property order is valid", + "data": {"baz": "bax", "foo": "bar"}, + "valid": true + }, + { + "description": "another object is invalid", + "data": {"foo": "bar"}, + "valid": false + }, + { + "description": "another type is invalid", + "data": [1, 2], + "valid": false + } + ] + }, + { + "description": "const with array", + "schema": {"const": [{ "foo": "bar" }]}, + "tests": [ + { + "description": "same array is valid", + "data": [{"foo": "bar"}], + "valid": true + }, + { + "description": "another array item is invalid", + "data": [2], + "valid": false + }, + { + "description": "array with additional items is invalid", + "data": [1, 2, 3], + "valid": false + } + ] + }, + { + "description": "const with null", + "schema": {"const": null}, + "tests": [ + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "not null is invalid", + "data": 0, + "valid": false + } + ] + }, + { + "description": "const with false does not match 0", + "schema": {"const": false}, + "tests": [ + { + "description": "false is valid", + "data": false, + "valid": true + }, + { + "description": "integer zero is invalid", + "data": 0, + "valid": false + }, + { + "description": "float zero is invalid", + "data": 0.0, + "valid": false + } + ] + }, + { + "description": "const with true does not match 1", + "schema": {"const": true}, + "tests": [ + { + "description": "true is valid", + "data": true, + "valid": true + }, + { + "description": "integer one is invalid", + "data": 1, + "valid": false + }, + { + "description": "float one is invalid", + "data": 1.0, + "valid": false + } + ] + }, + { + "description": "const with [false] does not match [0]", + "schema": {"const": [false]}, + "tests": [ + { + "description": "[false] is valid", + "data": [false], + "valid": true + }, + { + "description": "[0] is invalid", + "data": [0], + "valid": false + }, + { + "description": "[0.0] is invalid", + "data": [0.0], + "valid": false + } + ] + }, + { + "description": "const with [true] does not match [1]", + "schema": {"const": [true]}, + "tests": [ + { + "description": "[true] is valid", + "data": [true], + "valid": true + }, + { + "description": "[1] is invalid", + "data": [1], + "valid": false + }, + { + "description": "[1.0] is invalid", + "data": [1.0], + "valid": false + } + ] + }, + { + "description": "const with {\"a\": false} does not match {\"a\": 0}", + "schema": {"const": {"a": false}}, + "tests": [ + { + "description": "{\"a\": false} is valid", + "data": {"a": false}, + "valid": true + }, + { + "description": "{\"a\": 0} is invalid", + "data": {"a": 0}, + "valid": false + }, + { + "description": "{\"a\": 0.0} is invalid", + "data": {"a": 0.0}, + "valid": false + } + ] + }, + { + "description": "const with {\"a\": true} does not match {\"a\": 1}", + "schema": {"const": {"a": true}}, + "tests": [ + { + "description": "{\"a\": true} is valid", + "data": {"a": true}, + "valid": true + }, + { + "description": "{\"a\": 1} is invalid", + "data": {"a": 1}, + "valid": false + }, + { + "description": "{\"a\": 1.0} is invalid", + "data": {"a": 1.0}, + "valid": false + } + ] + }, + { + "description": "const with 0 does not match other zero-like types", + "schema": {"const": 0}, + "tests": [ + { + "description": "false is invalid", + "data": false, + "valid": false + }, + { + "description": "integer zero is valid", + "data": 0, + "valid": true + }, + { + "description": "float zero is valid", + "data": 0.0, + "valid": true + }, + { + "description": "empty object is invalid", + "data": {}, + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + }, + { + "description": "empty string is invalid", + "data": "", + "valid": false + } + ] + }, + { + "description": "const with 1 does not match true", + "schema": {"const": 1}, + "tests": [ + { + "description": "true is invalid", + "data": true, + "valid": false + }, + { + "description": "integer one is valid", + "data": 1, + "valid": true + }, + { + "description": "float one is valid", + "data": 1.0, + "valid": true + } + ] + }, + { + "description": "const with -2.0 matches integer and float types", + "schema": {"const": -2.0}, + "tests": [ + { + "description": "integer -2 is valid", + "data": -2, + "valid": true + }, + { + "description": "integer 2 is invalid", + "data": 2, + "valid": false + }, + { + "description": "float -2.0 is valid", + "data": -2.0, + "valid": true + }, + { + "description": "float 2.0 is invalid", + "data": 2.0, + "valid": false + }, + { + "description": "float -2.00001 is invalid", + "data": -2.00001, + "valid": false + } + ] + }, + { + "description": "float and integers are equal up to 64-bit representation limits", + "schema": {"const": 9007199254740992}, + "tests": [ + { + "description": "integer is valid", + "data": 9007199254740992, + "valid": true + }, + { + "description": "integer minus one is invalid", + "data": 9007199254740991, + "valid": false + }, + { + "description": "float is valid", + "data": 9007199254740992.0, + "valid": true + }, + { + "description": "float minus one is invalid", + "data": 9007199254740991.0, + "valid": false + } + ] + }, + { + "description": "nul characters in strings", + "schema": { "const": "hello\u0000there" }, + "tests": [ + { + "description": "match string with nul", + "data": "hello\u0000there", + "valid": true + }, + { + "description": "do not match string lacking nul", + "data": "hellothere", + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/contains.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/contains.json new file mode 100755 index 000000000000..c5471cc02108 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/contains.json @@ -0,0 +1,129 @@ +[ + { + "description": "contains keyword validation", + "schema": { + "contains": {"minimum": 5} + }, + "tests": [ + { + "description": "array with item matching schema (5) is valid", + "data": [3, 4, 5], + "valid": true + }, + { + "description": "array with item matching schema (6) is valid", + "data": [3, 4, 6], + "valid": true + }, + { + "description": "array with two items matching schema (5, 6) is valid", + "data": [3, 4, 5, 6], + "valid": true + }, + { + "description": "array without items matching schema is invalid", + "data": [2, 3, 4], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + }, + { + "description": "not array is valid", + "data": {}, + "valid": true + } + ] + }, + { + "description": "contains keyword with const keyword", + "schema": { + "contains": { "const": 5 } + }, + "tests": [ + { + "description": "array with item 5 is valid", + "data": [3, 4, 5], + "valid": true + }, + { + "description": "array with two items 5 is valid", + "data": [3, 4, 5, 5], + "valid": true + }, + { + "description": "array without item 5 is invalid", + "data": [1, 2, 3, 4], + "valid": false + } + ] + }, + { + "description": "contains keyword with boolean schema true", + "schema": {"contains": true}, + "tests": [ + { + "description": "any non-empty array is valid", + "data": ["foo"], + "valid": true + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + } + ] + }, + { + "description": "contains keyword with boolean schema false", + "schema": {"contains": false}, + "tests": [ + { + "description": "any non-empty array is invalid", + "data": ["foo"], + "valid": false + }, + { + "description": "empty array is invalid", + "data": [], + "valid": false + }, + { + "description": "non-arrays are valid", + "data": "contains does not apply to strings", + "valid": true + } + ] + }, + { + "description": "items + contains", + "schema": { + "items": { "multipleOf": 2 }, + "contains": { "multipleOf": 3 } + }, + "tests": [ + { + "description": "matches items, does not match contains", + "data": [ 2, 4, 8 ], + "valid": false + }, + { + "description": "does not match items, matches contains", + "data": [ 3, 6, 9 ], + "valid": false + }, + { + "description": "matches both items and contains", + "data": [ 6, 12 ], + "valid": true + }, + { + "description": "matches neither items nor contains", + "data": [ 1, 5 ], + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/default.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/default.json new file mode 100755 index 000000000000..289a9b66c13d --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/default.json @@ -0,0 +1,79 @@ +[ + { + "description": "invalid type for default", + "schema": { + "properties": { + "foo": { + "type": "integer", + "default": [] + } + } + }, + "tests": [ + { + "description": "valid when property is specified", + "data": {"foo": 13}, + "valid": true + }, + { + "description": "still valid when the invalid default is used", + "data": {}, + "valid": true + } + ] + }, + { + "description": "invalid string value for default", + "schema": { + "properties": { + "bar": { + "type": "string", + "minLength": 4, + "default": "bad" + } + } + }, + "tests": [ + { + "description": "valid when property is specified", + "data": {"bar": "good"}, + "valid": true + }, + { + "description": "still valid when the invalid default is used", + "data": {}, + "valid": true + } + ] + }, + { + "description": "the default keyword does not do anything if the property is missing", + "schema": { + "type": "object", + "properties": { + "alpha": { + "type": "number", + "maximum": 3, + "default": 5 + } + } + }, + "tests": [ + { + "description": "an explicit property value is checked against maximum (passing)", + "data": { "alpha": 1 }, + "valid": true + }, + { + "description": "an explicit property value is checked against maximum (failing)", + "data": { "alpha": 5 }, + "valid": false + }, + { + "description": "missing properties are not filled in with the default", + "data": {}, + "valid": true + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/definitions.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/definitions.json new file mode 100755 index 000000000000..d772fde3ec58 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/definitions.json @@ -0,0 +1,26 @@ +[ + { + "description": "validate definition against metaschema", + "schema": {"$ref": "http://json-schema.org/draft-06/schema#"}, + "tests": [ + { + "description": "valid definition schema", + "data": { + "definitions": { + "foo": {"type": "integer"} + } + }, + "valid": true + }, + { + "description": "invalid definition schema", + "data": { + "definitions": { + "foo": {"type": 1} + } + }, + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/dependencies.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/dependencies.json new file mode 100755 index 000000000000..a5e54282cccd --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/dependencies.json @@ -0,0 +1,248 @@ +[ + { + "description": "dependencies", + "schema": { + "dependencies": {"bar": ["foo"]} + }, + "tests": [ + { + "description": "neither", + "data": {}, + "valid": true + }, + { + "description": "nondependant", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "with dependency", + "data": {"foo": 1, "bar": 2}, + "valid": true + }, + { + "description": "missing dependency", + "data": {"bar": 2}, + "valid": false + }, + { + "description": "ignores arrays", + "data": ["bar"], + "valid": true + }, + { + "description": "ignores strings", + "data": "foobar", + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + } + ] + }, + { + "description": "dependencies with empty array", + "schema": { + "dependencies": {"bar": []} + }, + "tests": [ + { + "description": "empty object", + "data": {}, + "valid": true + }, + { + "description": "object with one property", + "data": {"bar": 2}, + "valid": true + }, + { + "description": "non-object is valid", + "data": 1, + "valid": true + } + ] + }, + { + "description": "multiple dependencies", + "schema": { + "dependencies": {"quux": ["foo", "bar"]} + }, + "tests": [ + { + "description": "neither", + "data": {}, + "valid": true + }, + { + "description": "nondependants", + "data": {"foo": 1, "bar": 2}, + "valid": true + }, + { + "description": "with dependencies", + "data": {"foo": 1, "bar": 2, "quux": 3}, + "valid": true + }, + { + "description": "missing dependency", + "data": {"foo": 1, "quux": 2}, + "valid": false + }, + { + "description": "missing other dependency", + "data": {"bar": 1, "quux": 2}, + "valid": false + }, + { + "description": "missing both dependencies", + "data": {"quux": 1}, + "valid": false + } + ] + }, + { + "description": "multiple dependencies subschema", + "schema": { + "dependencies": { + "bar": { + "properties": { + "foo": {"type": "integer"}, + "bar": {"type": "integer"} + } + } + } + }, + "tests": [ + { + "description": "valid", + "data": {"foo": 1, "bar": 2}, + "valid": true + }, + { + "description": "no dependency", + "data": {"foo": "quux"}, + "valid": true + }, + { + "description": "wrong type", + "data": {"foo": "quux", "bar": 2}, + "valid": false + }, + { + "description": "wrong type other", + "data": {"foo": 2, "bar": "quux"}, + "valid": false + }, + { + "description": "wrong type both", + "data": {"foo": "quux", "bar": "quux"}, + "valid": false + } + ] + }, + { + "description": "dependencies with boolean subschemas", + "schema": { + "dependencies": { + "foo": true, + "bar": false + } + }, + "tests": [ + { + "description": "object with property having schema true is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "object with property having schema false is invalid", + "data": {"bar": 2}, + "valid": false + }, + { + "description": "object with both properties is invalid", + "data": {"foo": 1, "bar": 2}, + "valid": false + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + } + ] + }, + { + "description": "dependencies with escaped characters", + "schema": { + "dependencies": { + "foo\nbar": ["foo\rbar"], + "foo\tbar": { + "minProperties": 4 + }, + "foo'bar": {"required": ["foo\"bar"]}, + "foo\"bar": ["foo'bar"] + } + }, + "tests": [ + { + "description": "valid object 1", + "data": { + "foo\nbar": 1, + "foo\rbar": 2 + }, + "valid": true + }, + { + "description": "valid object 2", + "data": { + "foo\tbar": 1, + "a": 2, + "b": 3, + "c": 4 + }, + "valid": true + }, + { + "description": "valid object 3", + "data": { + "foo'bar": 1, + "foo\"bar": 2 + }, + "valid": true + }, + { + "description": "invalid object 1", + "data": { + "foo\nbar": 1, + "foo": 2 + }, + "valid": false + }, + { + "description": "invalid object 2", + "data": { + "foo\tbar": 1, + "a": 2 + }, + "valid": false + }, + { + "description": "invalid object 3", + "data": { + "foo'bar": 1 + }, + "valid": false + }, + { + "description": "invalid object 4", + "data": { + "foo\"bar": 2 + }, + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/enum.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/enum.json new file mode 100755 index 000000000000..f085097be77a --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/enum.json @@ -0,0 +1,236 @@ +[ + { + "description": "simple enum validation", + "schema": {"enum": [1, 2, 3]}, + "tests": [ + { + "description": "one of the enum is valid", + "data": 1, + "valid": true + }, + { + "description": "something else is invalid", + "data": 4, + "valid": false + } + ] + }, + { + "description": "heterogeneous enum validation", + "schema": {"enum": [6, "foo", [], true, {"foo": 12}]}, + "tests": [ + { + "description": "one of the enum is valid", + "data": [], + "valid": true + }, + { + "description": "something else is invalid", + "data": null, + "valid": false + }, + { + "description": "objects are deep compared", + "data": {"foo": false}, + "valid": false + }, + { + "description": "valid object matches", + "data": {"foo": 12}, + "valid": true + }, + { + "description": "extra properties in object is invalid", + "data": {"foo": 12, "boo": 42}, + "valid": false + } + ] + }, + { + "description": "heterogeneous enum-with-null validation", + "schema": { "enum": [6, null] }, + "tests": [ + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "number is valid", + "data": 6, + "valid": true + }, + { + "description": "something else is invalid", + "data": "test", + "valid": false + } + ] + }, + { + "description": "enums in properties", + "schema": { + "type":"object", + "properties": { + "foo": {"enum":["foo"]}, + "bar": {"enum":["bar"]} + }, + "required": ["bar"] + }, + "tests": [ + { + "description": "both properties are valid", + "data": {"foo":"foo", "bar":"bar"}, + "valid": true + }, + { + "description": "wrong foo value", + "data": {"foo":"foot", "bar":"bar"}, + "valid": false + }, + { + "description": "wrong bar value", + "data": {"foo":"foo", "bar":"bart"}, + "valid": false + }, + { + "description": "missing optional property is valid", + "data": {"bar":"bar"}, + "valid": true + }, + { + "description": "missing required property is invalid", + "data": {"foo":"foo"}, + "valid": false + }, + { + "description": "missing all properties is invalid", + "data": {}, + "valid": false + } + ] + }, + { + "description": "enum with escaped characters", + "schema": { + "enum": ["foo\nbar", "foo\rbar"] + }, + "tests": [ + { + "description": "member 1 is valid", + "data": "foo\nbar", + "valid": true + }, + { + "description": "member 2 is valid", + "data": "foo\rbar", + "valid": true + }, + { + "description": "another string is invalid", + "data": "abc", + "valid": false + } + ] + }, + { + "description": "enum with false does not match 0", + "schema": {"enum": [false]}, + "tests": [ + { + "description": "false is valid", + "data": false, + "valid": true + }, + { + "description": "integer zero is invalid", + "data": 0, + "valid": false + }, + { + "description": "float zero is invalid", + "data": 0.0, + "valid": false + } + ] + }, + { + "description": "enum with true does not match 1", + "schema": {"enum": [true]}, + "tests": [ + { + "description": "true is valid", + "data": true, + "valid": true + }, + { + "description": "integer one is invalid", + "data": 1, + "valid": false + }, + { + "description": "float one is invalid", + "data": 1.0, + "valid": false + } + ] + }, + { + "description": "enum with 0 does not match false", + "schema": {"enum": [0]}, + "tests": [ + { + "description": "false is invalid", + "data": false, + "valid": false + }, + { + "description": "integer zero is valid", + "data": 0, + "valid": true + }, + { + "description": "float zero is valid", + "data": 0.0, + "valid": true + } + ] + }, + { + "description": "enum with 1 does not match true", + "schema": {"enum": [1]}, + "tests": [ + { + "description": "true is invalid", + "data": true, + "valid": false + }, + { + "description": "integer one is valid", + "data": 1, + "valid": true + }, + { + "description": "float one is valid", + "data": 1.0, + "valid": true + } + ] + }, + { + "description": "nul characters in strings", + "schema": { "enum": [ "hello\u0000there" ] }, + "tests": [ + { + "description": "match string with nul", + "data": "hello\u0000there", + "valid": true + }, + { + "description": "do not match string lacking nul", + "data": "hellothere", + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/exclusiveMaximum.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/exclusiveMaximum.json new file mode 100755 index 000000000000..dc3cd709d31d --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/exclusiveMaximum.json @@ -0,0 +1,30 @@ +[ + { + "description": "exclusiveMaximum validation", + "schema": { + "exclusiveMaximum": 3.0 + }, + "tests": [ + { + "description": "below the exclusiveMaximum is valid", + "data": 2.2, + "valid": true + }, + { + "description": "boundary point is invalid", + "data": 3.0, + "valid": false + }, + { + "description": "above the exclusiveMaximum is invalid", + "data": 3.5, + "valid": false + }, + { + "description": "ignores non-numbers", + "data": "x", + "valid": true + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/exclusiveMinimum.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/exclusiveMinimum.json new file mode 100755 index 000000000000..b38d7ecec630 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/exclusiveMinimum.json @@ -0,0 +1,30 @@ +[ + { + "description": "exclusiveMinimum validation", + "schema": { + "exclusiveMinimum": 1.1 + }, + "tests": [ + { + "description": "above the exclusiveMinimum is valid", + "data": 1.2, + "valid": true + }, + { + "description": "boundary point is invalid", + "data": 1.1, + "valid": false + }, + { + "description": "below the exclusiveMinimum is invalid", + "data": 0.6, + "valid": false + }, + { + "description": "ignores non-numbers", + "data": "x", + "valid": true + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/format.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/format.json new file mode 100755 index 000000000000..2df2a9f0a559 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/format.json @@ -0,0 +1,326 @@ +[ + { + "description": "email format", + "schema": { "format": "email" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + }, + { + "description": "ipv4 format", + "schema": { "format": "ipv4" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + }, + { + "description": "ipv6 format", + "schema": { "format": "ipv6" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + }, + { + "description": "hostname format", + "schema": { "format": "hostname" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + }, + { + "description": "date-time format", + "schema": { "format": "date-time" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + }, + { + "description": "json-pointer format", + "schema": { "format": "json-pointer" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + }, + { + "description": "uri format", + "schema": { "format": "uri" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + }, + { + "description": "uri-reference format", + "schema": { "format": "uri-reference" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + }, + { + "description": "uri-template format", + "schema": { "format": "uri-template" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/id.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/id.json new file mode 100755 index 000000000000..b58e0d0071d0 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/id.json @@ -0,0 +1,53 @@ +[ + { + "description": "id inside an enum is not a real identifier", + "comment": "the implementation must not be confused by an id buried in the enum", + "schema": { + "definitions": { + "id_in_enum": { + "enum": [ + { + "$id": "https://localhost:1234/id/my_identifier.json", + "type": "null" + } + ] + }, + "real_id_in_schema": { + "$id": "https://localhost:1234/id/my_identifier.json", + "type": "string" + }, + "zzz_id_in_const": { + "const": { + "$id": "https://localhost:1234/id/my_identifier.json", + "type": "null" + } + } + }, + "anyOf": [ + { "$ref": "#/definitions/id_in_enum" }, + { "$ref": "https://localhost:1234/id/my_identifier.json" } + ] + }, + "tests": [ + { + "description": "exact match to enum, and type matches", + "data": { + "$id": "https://localhost:1234/id/my_identifier.json", + "type": "null" + }, + "valid": true + }, + { + "description": "match $ref to id", + "data": "a string to match #/definitions/id_in_enum", + "valid": true + }, + { + "description": "no match on enum or $ref to id", + "data": 1, + "valid": false + } + ] + } + +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/infinite-loop-detection.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/infinite-loop-detection.json new file mode 100755 index 000000000000..f98c74fc6409 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/infinite-loop-detection.json @@ -0,0 +1,36 @@ +[ + { + "description": "evaluating the same schema location against the same data location twice is not a sign of an infinite loop", + "schema": { + "definitions": { + "int": { "type": "integer" } + }, + "allOf": [ + { + "properties": { + "foo": { + "$ref": "#/definitions/int" + } + } + }, + { + "additionalProperties": { + "$ref": "#/definitions/int" + } + } + ] + }, + "tests": [ + { + "description": "passing case", + "data": { "foo": 1 }, + "valid": true + }, + { + "description": "failing case", + "data": { "foo": "a string" }, + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/items.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/items.json new file mode 100755 index 000000000000..67f11840a24d --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/items.json @@ -0,0 +1,250 @@ +[ + { + "description": "a schema given for items", + "schema": { + "items": {"type": "integer"} + }, + "tests": [ + { + "description": "valid items", + "data": [ 1, 2, 3 ], + "valid": true + }, + { + "description": "wrong type of items", + "data": [1, "x"], + "valid": false + }, + { + "description": "ignores non-arrays", + "data": {"foo" : "bar"}, + "valid": true + }, + { + "description": "JavaScript pseudo-array is valid", + "data": { + "0": "invalid", + "length": 1 + }, + "valid": true + } + ] + }, + { + "description": "an array of schemas for items", + "schema": { + "items": [ + {"type": "integer"}, + {"type": "string"} + ] + }, + "tests": [ + { + "description": "correct types", + "data": [ 1, "foo" ], + "valid": true + }, + { + "description": "wrong types", + "data": [ "foo", 1 ], + "valid": false + }, + { + "description": "incomplete array of items", + "data": [ 1 ], + "valid": true + }, + { + "description": "array with additional items", + "data": [ 1, "foo", true ], + "valid": true + }, + { + "description": "empty array", + "data": [ ], + "valid": true + }, + { + "description": "JavaScript pseudo-array is valid", + "data": { + "0": "invalid", + "1": "valid", + "length": 2 + }, + "valid": true + } + ] + }, + { + "description": "items with boolean schema (true)", + "schema": {"items": true}, + "tests": [ + { + "description": "any array is valid", + "data": [ 1, "foo", true ], + "valid": true + }, + { + "description": "empty array is valid", + "data": [], + "valid": true + } + ] + }, + { + "description": "items with boolean schema (false)", + "schema": {"items": false}, + "tests": [ + { + "description": "any non-empty array is invalid", + "data": [ 1, "foo", true ], + "valid": false + }, + { + "description": "empty array is valid", + "data": [], + "valid": true + } + ] + }, + { + "description": "items with boolean schemas", + "schema": { + "items": [true, false] + }, + "tests": [ + { + "description": "array with one item is valid", + "data": [ 1 ], + "valid": true + }, + { + "description": "array with two items is invalid", + "data": [ 1, "foo" ], + "valid": false + }, + { + "description": "empty array is valid", + "data": [], + "valid": true + } + ] + }, + { + "description": "items and subitems", + "schema": { + "definitions": { + "item": { + "type": "array", + "additionalItems": false, + "items": [ + { "$ref": "#/definitions/sub-item" }, + { "$ref": "#/definitions/sub-item" } + ] + }, + "sub-item": { + "type": "object", + "required": ["foo"] + } + }, + "type": "array", + "additionalItems": false, + "items": [ + { "$ref": "#/definitions/item" }, + { "$ref": "#/definitions/item" }, + { "$ref": "#/definitions/item" } + ] + }, + "tests": [ + { + "description": "valid items", + "data": [ + [ {"foo": null}, {"foo": null} ], + [ {"foo": null}, {"foo": null} ], + [ {"foo": null}, {"foo": null} ] + ], + "valid": true + }, + { + "description": "too many items", + "data": [ + [ {"foo": null}, {"foo": null} ], + [ {"foo": null}, {"foo": null} ], + [ {"foo": null}, {"foo": null} ], + [ {"foo": null}, {"foo": null} ] + ], + "valid": false + }, + { + "description": "too many sub-items", + "data": [ + [ {"foo": null}, {"foo": null}, {"foo": null} ], + [ {"foo": null}, {"foo": null} ], + [ {"foo": null}, {"foo": null} ] + ], + "valid": false + }, + { + "description": "wrong item", + "data": [ + {"foo": null}, + [ {"foo": null}, {"foo": null} ], + [ {"foo": null}, {"foo": null} ] + ], + "valid": false + }, + { + "description": "wrong sub-item", + "data": [ + [ {}, {"foo": null} ], + [ {"foo": null}, {"foo": null} ], + [ {"foo": null}, {"foo": null} ] + ], + "valid": false + }, + { + "description": "fewer items is valid", + "data": [ + [ {"foo": null} ], + [ {"foo": null} ] + ], + "valid": true + } + ] + }, + { + "description": "nested items", + "schema": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "number" + } + } + } + } + }, + "tests": [ + { + "description": "valid nested array", + "data": [[[[1]], [[2],[3]]], [[[4], [5], [6]]]], + "valid": true + }, + { + "description": "nested array with invalid type", + "data": [[[["1"]], [[2],[3]]], [[[4], [5], [6]]]], + "valid": false + }, + { + "description": "not deep enough", + "data": [[[1], [2],[3]], [[4], [5], [6]]], + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/maxItems.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/maxItems.json new file mode 100755 index 000000000000..3b53a6b371a7 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/maxItems.json @@ -0,0 +1,28 @@ +[ + { + "description": "maxItems validation", + "schema": {"maxItems": 2}, + "tests": [ + { + "description": "shorter is valid", + "data": [1], + "valid": true + }, + { + "description": "exact length is valid", + "data": [1, 2], + "valid": true + }, + { + "description": "too long is invalid", + "data": [1, 2, 3], + "valid": false + }, + { + "description": "ignores non-arrays", + "data": "foobar", + "valid": true + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/maxLength.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/maxLength.json new file mode 100755 index 000000000000..811d35b253c0 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/maxLength.json @@ -0,0 +1,33 @@ +[ + { + "description": "maxLength validation", + "schema": {"maxLength": 2}, + "tests": [ + { + "description": "shorter is valid", + "data": "f", + "valid": true + }, + { + "description": "exact length is valid", + "data": "fo", + "valid": true + }, + { + "description": "too long is invalid", + "data": "foo", + "valid": false + }, + { + "description": "ignores non-strings", + "data": 100, + "valid": true + }, + { + "description": "two supplementary Unicode code points is long enough", + "data": "\uD83D\uDCA9\uD83D\uDCA9", + "valid": true + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/maxProperties.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/maxProperties.json new file mode 100755 index 000000000000..aa7209f537c1 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/maxProperties.json @@ -0,0 +1,54 @@ +[ + { + "description": "maxProperties validation", + "schema": {"maxProperties": 2}, + "tests": [ + { + "description": "shorter is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "exact length is valid", + "data": {"foo": 1, "bar": 2}, + "valid": true + }, + { + "description": "too long is invalid", + "data": {"foo": 1, "bar": 2, "baz": 3}, + "valid": false + }, + { + "description": "ignores arrays", + "data": [1, 2, 3], + "valid": true + }, + { + "description": "ignores strings", + "data": "foobar", + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + } + ] + }, + { + "description": "maxProperties = 0 means the object is empty", + "schema": { "maxProperties": 0 }, + "tests": [ + { + "description": "no properties is valid", + "data": {}, + "valid": true + }, + { + "description": "one property is invalid", + "data": { "foo": 1 }, + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/maximum.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/maximum.json new file mode 100755 index 000000000000..6844a39eee09 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/maximum.json @@ -0,0 +1,54 @@ +[ + { + "description": "maximum validation", + "schema": {"maximum": 3.0}, + "tests": [ + { + "description": "below the maximum is valid", + "data": 2.6, + "valid": true + }, + { + "description": "boundary point is valid", + "data": 3.0, + "valid": true + }, + { + "description": "above the maximum is invalid", + "data": 3.5, + "valid": false + }, + { + "description": "ignores non-numbers", + "data": "x", + "valid": true + } + ] + }, + { + "description": "maximum validation with unsigned integer", + "schema": {"maximum": 300}, + "tests": [ + { + "description": "below the maximum is invalid", + "data": 299.97, + "valid": true + }, + { + "description": "boundary point integer is valid", + "data": 300, + "valid": true + }, + { + "description": "boundary point float is valid", + "data": 300.00, + "valid": true + }, + { + "description": "above the maximum is invalid", + "data": 300.5, + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/minItems.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/minItems.json new file mode 100755 index 000000000000..ed5118815ee9 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/minItems.json @@ -0,0 +1,28 @@ +[ + { + "description": "minItems validation", + "schema": {"minItems": 1}, + "tests": [ + { + "description": "longer is valid", + "data": [1, 2], + "valid": true + }, + { + "description": "exact length is valid", + "data": [1], + "valid": true + }, + { + "description": "too short is invalid", + "data": [], + "valid": false + }, + { + "description": "ignores non-arrays", + "data": "", + "valid": true + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/minLength.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/minLength.json new file mode 100755 index 000000000000..3f09158deef0 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/minLength.json @@ -0,0 +1,33 @@ +[ + { + "description": "minLength validation", + "schema": {"minLength": 2}, + "tests": [ + { + "description": "longer is valid", + "data": "foo", + "valid": true + }, + { + "description": "exact length is valid", + "data": "fo", + "valid": true + }, + { + "description": "too short is invalid", + "data": "f", + "valid": false + }, + { + "description": "ignores non-strings", + "data": 1, + "valid": true + }, + { + "description": "one supplementary Unicode code point is not long enough", + "data": "\uD83D\uDCA9", + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/minProperties.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/minProperties.json new file mode 100755 index 000000000000..49a0726e01ce --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/minProperties.json @@ -0,0 +1,38 @@ +[ + { + "description": "minProperties validation", + "schema": {"minProperties": 1}, + "tests": [ + { + "description": "longer is valid", + "data": {"foo": 1, "bar": 2}, + "valid": true + }, + { + "description": "exact length is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "too short is invalid", + "data": {}, + "valid": false + }, + { + "description": "ignores arrays", + "data": [], + "valid": true + }, + { + "description": "ignores strings", + "data": "", + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/minimum.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/minimum.json new file mode 100755 index 000000000000..21ae50e0ed7b --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/minimum.json @@ -0,0 +1,69 @@ +[ + { + "description": "minimum validation", + "schema": {"minimum": 1.1}, + "tests": [ + { + "description": "above the minimum is valid", + "data": 2.6, + "valid": true + }, + { + "description": "boundary point is valid", + "data": 1.1, + "valid": true + }, + { + "description": "below the minimum is invalid", + "data": 0.6, + "valid": false + }, + { + "description": "ignores non-numbers", + "data": "x", + "valid": true + } + ] + }, + { + "description": "minimum validation with signed integer", + "schema": {"minimum": -2}, + "tests": [ + { + "description": "negative above the minimum is valid", + "data": -1, + "valid": true + }, + { + "description": "positive above the minimum is valid", + "data": 0, + "valid": true + }, + { + "description": "boundary point is valid", + "data": -2, + "valid": true + }, + { + "description": "boundary point with float is valid", + "data": -2.0, + "valid": true + }, + { + "description": "float below the minimum is invalid", + "data": -2.0001, + "valid": false + }, + { + "description": "int below the minimum is invalid", + "data": -3, + "valid": false + }, + { + "description": "ignores non-numbers", + "data": "x", + "valid": true + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/multipleOf.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/multipleOf.json new file mode 100755 index 000000000000..faa87cff5905 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/multipleOf.json @@ -0,0 +1,71 @@ +[ + { + "description": "by int", + "schema": {"multipleOf": 2}, + "tests": [ + { + "description": "int by int", + "data": 10, + "valid": true + }, + { + "description": "int by int fail", + "data": 7, + "valid": false + }, + { + "description": "ignores non-numbers", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "by number", + "schema": {"multipleOf": 1.5}, + "tests": [ + { + "description": "zero is multiple of anything", + "data": 0, + "valid": true + }, + { + "description": "4.5 is multiple of 1.5", + "data": 4.5, + "valid": true + }, + { + "description": "35 is not multiple of 1.5", + "data": 35, + "valid": false + } + ] + }, + { + "description": "by small number", + "schema": {"multipleOf": 0.0001}, + "tests": [ + { + "description": "0.0075 is multiple of 0.0001", + "data": 0.0075, + "valid": true + }, + { + "description": "0.00751 is not multiple of 0.0001", + "data": 0.00751, + "valid": false + } + ] + }, + { + "description": "invalid instance should not raise error when float division = inf", + "schema": {"type": "integer", "multipleOf": 0.123456789}, + "tests": [ + { + "description": "always invalid, but naive implementations may raise an overflow error", + "data": 1e308, + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/not.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/not.json new file mode 100755 index 000000000000..98de0eda8dd0 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/not.json @@ -0,0 +1,117 @@ +[ + { + "description": "not", + "schema": { + "not": {"type": "integer"} + }, + "tests": [ + { + "description": "allowed", + "data": "foo", + "valid": true + }, + { + "description": "disallowed", + "data": 1, + "valid": false + } + ] + }, + { + "description": "not multiple types", + "schema": { + "not": {"type": ["integer", "boolean"]} + }, + "tests": [ + { + "description": "valid", + "data": "foo", + "valid": true + }, + { + "description": "mismatch", + "data": 1, + "valid": false + }, + { + "description": "other mismatch", + "data": true, + "valid": false + } + ] + }, + { + "description": "not more complex schema", + "schema": { + "not": { + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } + } + }, + "tests": [ + { + "description": "match", + "data": 1, + "valid": true + }, + { + "description": "other match", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "mismatch", + "data": {"foo": "bar"}, + "valid": false + } + ] + }, + { + "description": "forbidden property", + "schema": { + "properties": { + "foo": { + "not": {} + } + } + }, + "tests": [ + { + "description": "property present", + "data": {"foo": 1, "bar": 2}, + "valid": false + }, + { + "description": "property absent", + "data": {"bar": 1, "baz": 2}, + "valid": true + } + ] + }, + { + "description": "not with boolean schema true", + "schema": {"not": true}, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "not with boolean schema false", + "schema": {"not": false}, + "tests": [ + { + "description": "any value is valid", + "data": "foo", + "valid": true + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/oneOf.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/oneOf.json new file mode 100755 index 000000000000..eeb7ae866ad6 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/oneOf.json @@ -0,0 +1,274 @@ +[ + { + "description": "oneOf", + "schema": { + "oneOf": [ + { + "type": "integer" + }, + { + "minimum": 2 + } + ] + }, + "tests": [ + { + "description": "first oneOf valid", + "data": 1, + "valid": true + }, + { + "description": "second oneOf valid", + "data": 2.5, + "valid": true + }, + { + "description": "both oneOf valid", + "data": 3, + "valid": false + }, + { + "description": "neither oneOf valid", + "data": 1.5, + "valid": false + } + ] + }, + { + "description": "oneOf with base schema", + "schema": { + "type": "string", + "oneOf" : [ + { + "minLength": 2 + }, + { + "maxLength": 4 + } + ] + }, + "tests": [ + { + "description": "mismatch base schema", + "data": 3, + "valid": false + }, + { + "description": "one oneOf valid", + "data": "foobar", + "valid": true + }, + { + "description": "both oneOf valid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "oneOf with boolean schemas, all true", + "schema": {"oneOf": [true, true, true]}, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "oneOf with boolean schemas, one true", + "schema": {"oneOf": [true, false, false]}, + "tests": [ + { + "description": "any value is valid", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "oneOf with boolean schemas, more than one true", + "schema": {"oneOf": [true, true, false]}, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "oneOf with boolean schemas, all false", + "schema": {"oneOf": [false, false, false]}, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "oneOf complex types", + "schema": { + "oneOf": [ + { + "properties": { + "bar": {"type": "integer"} + }, + "required": ["bar"] + }, + { + "properties": { + "foo": {"type": "string"} + }, + "required": ["foo"] + } + ] + }, + "tests": [ + { + "description": "first oneOf valid (complex)", + "data": {"bar": 2}, + "valid": true + }, + { + "description": "second oneOf valid (complex)", + "data": {"foo": "baz"}, + "valid": true + }, + { + "description": "both oneOf valid (complex)", + "data": {"foo": "baz", "bar": 2}, + "valid": false + }, + { + "description": "neither oneOf valid (complex)", + "data": {"foo": 2, "bar": "quux"}, + "valid": false + } + ] + }, + { + "description": "oneOf with empty schema", + "schema": { + "oneOf": [ + { "type": "number" }, + {} + ] + }, + "tests": [ + { + "description": "one valid - valid", + "data": "foo", + "valid": true + }, + { + "description": "both valid - invalid", + "data": 123, + "valid": false + } + ] + }, + { + "description": "oneOf with required", + "schema": { + "type": "object", + "oneOf": [ + { "required": ["foo", "bar"] }, + { "required": ["foo", "baz"] } + ] + }, + "tests": [ + { + "description": "both invalid - invalid", + "data": {"bar": 2}, + "valid": false + }, + { + "description": "first valid - valid", + "data": {"foo": 1, "bar": 2}, + "valid": true + }, + { + "description": "second valid - valid", + "data": {"foo": 1, "baz": 3}, + "valid": true + }, + { + "description": "both valid - invalid", + "data": {"foo": 1, "bar": 2, "baz" : 3}, + "valid": false + } + ] + }, + { + "description": "oneOf with missing optional property", + "schema": { + "oneOf": [ + { + "properties": { + "bar": true, + "baz": true + }, + "required": ["bar"] + }, + { + "properties": { + "foo": true + }, + "required": ["foo"] + } + ] + }, + "tests": [ + { + "description": "first oneOf valid", + "data": {"bar": 8}, + "valid": true + }, + { + "description": "second oneOf valid", + "data": {"foo": "foo"}, + "valid": true + }, + { + "description": "both oneOf valid", + "data": {"foo": "foo", "bar": 8}, + "valid": false + }, + { + "description": "neither oneOf valid", + "data": {"baz": "quux"}, + "valid": false + } + ] + }, + { + "description": "nested oneOf, to check validation semantics", + "schema": { + "oneOf": [ + { + "oneOf": [ + { + "type": "null" + } + ] + } + ] + }, + "tests": [ + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "anything non-null is invalid", + "data": 123, + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/bignum.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/bignum.json new file mode 100755 index 000000000000..3f49226ac396 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/bignum.json @@ -0,0 +1,93 @@ +[ + { + "description": "integer", + "schema": { "type": "integer" }, + "tests": [ + { + "description": "a bignum is an integer", + "data": 12345678910111213141516171819202122232425262728293031, + "valid": true + }, + { + "description": "a negative bignum is an integer", + "data": -12345678910111213141516171819202122232425262728293031, + "valid": true + } + ] + }, + { + "description": "number", + "schema": { "type": "number" }, + "tests": [ + { + "description": "a bignum is a number", + "data": 98249283749234923498293171823948729348710298301928331, + "valid": true + }, + { + "description": "a negative bignum is a number", + "data": -98249283749234923498293171823948729348710298301928331, + "valid": true + } + ] + }, + { + "description": "string", + "schema": { "type": "string" }, + "tests": [ + { + "description": "a bignum is not a string", + "data": 98249283749234923498293171823948729348710298301928331, + "valid": false + } + ] + }, + { + "description": "integer comparison", + "schema": { "maximum": 18446744073709551615 }, + "tests": [ + { + "description": "comparison works for high numbers", + "data": 18446744073709551600, + "valid": true + } + ] + }, + { + "description": "float comparison with high precision", + "schema": { + "exclusiveMaximum": 972783798187987123879878123.18878137 + }, + "tests": [ + { + "description": "comparison works for high numbers", + "data": 972783798187987123879878123.188781371, + "valid": false + } + ] + }, + { + "description": "integer comparison", + "schema": { "minimum": -18446744073709551615 }, + "tests": [ + { + "description": "comparison works for very negative numbers", + "data": -18446744073709551600, + "valid": true + } + ] + }, + { + "description": "float comparison with high precision on negative numbers", + "schema": { + "exclusiveMinimum": -972783798187987123879878123.18878137 + }, + "tests": [ + { + "description": "comparison works for very negative numbers", + "data": -972783798187987123879878123.188781371, + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/ecmascript-regex.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/ecmascript-regex.json new file mode 100755 index 000000000000..1beb0b3a661e --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/ecmascript-regex.json @@ -0,0 +1,552 @@ +[ + { + "description": "ECMA 262 regex $ does not match trailing newline", + "schema": { + "type": "string", + "pattern": "^abc$" + }, + "tests": [ + { + "description": "matches in Python, but should not in jsonschema", + "data": "abc\\n", + "valid": false + }, + { + "description": "should match", + "data": "abc", + "valid": true + } + ] + }, + { + "description": "ECMA 262 regex converts \\t to horizontal tab", + "schema": { + "type": "string", + "pattern": "^\\t$" + }, + "tests": [ + { + "description": "does not match", + "data": "\\t", + "valid": false + }, + { + "description": "matches", + "data": "\u0009", + "valid": true + } + ] + }, + { + "description": "ECMA 262 regex escapes control codes with \\c and upper letter", + "schema": { + "type": "string", + "pattern": "^\\cC$" + }, + "tests": [ + { + "description": "does not match", + "data": "\\cC", + "valid": false + }, + { + "description": "matches", + "data": "\u0003", + "valid": true + } + ] + }, + { + "description": "ECMA 262 regex escapes control codes with \\c and lower letter", + "schema": { + "type": "string", + "pattern": "^\\cc$" + }, + "tests": [ + { + "description": "does not match", + "data": "\\cc", + "valid": false + }, + { + "description": "matches", + "data": "\u0003", + "valid": true + } + ] + }, + { + "description": "ECMA 262 \\d matches ascii digits only", + "schema": { + "type": "string", + "pattern": "^\\d$" + }, + "tests": [ + { + "description": "ASCII zero matches", + "data": "0", + "valid": true + }, + { + "description": "NKO DIGIT ZERO does not match (unlike e.g. Python)", + "data": "߀", + "valid": false + }, + { + "description": "NKO DIGIT ZERO (as \\u escape) does not match", + "data": "\u07c0", + "valid": false + } + ] + }, + { + "description": "ECMA 262 \\D matches everything but ascii digits", + "schema": { + "type": "string", + "pattern": "^\\D$" + }, + "tests": [ + { + "description": "ASCII zero does not match", + "data": "0", + "valid": false + }, + { + "description": "NKO DIGIT ZERO matches (unlike e.g. Python)", + "data": "߀", + "valid": true + }, + { + "description": "NKO DIGIT ZERO (as \\u escape) matches", + "data": "\u07c0", + "valid": true + } + ] + }, + { + "description": "ECMA 262 \\w matches ascii letters only", + "schema": { + "type": "string", + "pattern": "^\\w$" + }, + "tests": [ + { + "description": "ASCII 'a' matches", + "data": "a", + "valid": true + }, + { + "description": "latin-1 e-acute does not match (unlike e.g. Python)", + "data": "é", + "valid": false + } + ] + }, + { + "description": "ECMA 262 \\W matches everything but ascii letters", + "schema": { + "type": "string", + "pattern": "^\\W$" + }, + "tests": [ + { + "description": "ASCII 'a' does not match", + "data": "a", + "valid": false + }, + { + "description": "latin-1 e-acute matches (unlike e.g. Python)", + "data": "é", + "valid": true + } + ] + }, + { + "description": "ECMA 262 \\s matches whitespace", + "schema": { + "type": "string", + "pattern": "^\\s$" + }, + "tests": [ + { + "description": "ASCII space matches", + "data": " ", + "valid": true + }, + { + "description": "Character tabulation matches", + "data": "\t", + "valid": true + }, + { + "description": "Line tabulation matches", + "data": "\u000b", + "valid": true + }, + { + "description": "Form feed matches", + "data": "\u000c", + "valid": true + }, + { + "description": "latin-1 non-breaking-space matches", + "data": "\u00a0", + "valid": true + }, + { + "description": "zero-width whitespace matches", + "data": "\ufeff", + "valid": true + }, + { + "description": "line feed matches (line terminator)", + "data": "\u000a", + "valid": true + }, + { + "description": "paragraph separator matches (line terminator)", + "data": "\u2029", + "valid": true + }, + { + "description": "EM SPACE matches (Space_Separator)", + "data": "\u2003", + "valid": true + }, + { + "description": "Non-whitespace control does not match", + "data": "\u0001", + "valid": false + }, + { + "description": "Non-whitespace does not match", + "data": "\u2013", + "valid": false + } + ] + }, + { + "description": "ECMA 262 \\S matches everything but whitespace", + "schema": { + "type": "string", + "pattern": "^\\S$" + }, + "tests": [ + { + "description": "ASCII space does not match", + "data": " ", + "valid": false + }, + { + "description": "Character tabulation does not match", + "data": "\t", + "valid": false + }, + { + "description": "Line tabulation does not match", + "data": "\u000b", + "valid": false + }, + { + "description": "Form feed does not match", + "data": "\u000c", + "valid": false + }, + { + "description": "latin-1 non-breaking-space does not match", + "data": "\u00a0", + "valid": false + }, + { + "description": "zero-width whitespace does not match", + "data": "\ufeff", + "valid": false + }, + { + "description": "line feed does not match (line terminator)", + "data": "\u000a", + "valid": false + }, + { + "description": "paragraph separator does not match (line terminator)", + "data": "\u2029", + "valid": false + }, + { + "description": "EM SPACE does not match (Space_Separator)", + "data": "\u2003", + "valid": false + }, + { + "description": "Non-whitespace control matches", + "data": "\u0001", + "valid": true + }, + { + "description": "Non-whitespace matches", + "data": "\u2013", + "valid": true + } + ] + }, + { + "description": "unicode semantics should be used for all pattern matching", + "schema": { "pattern": "\\p{Letter}cole" }, + "tests": [ + { + "description": "ascii character in json string", + "data": "Les hivers de mon enfance etaient des saisons longues, longues. Nous vivions en trois lieux: l'ecole, l'eglise et la patinoire; mais la vraie vie etait sur la patinoire.", + "valid": true + }, + { + "description": "literal unicode character in json string", + "data": "Les hivers de mon enfance étaient des saisons longues, longues. Nous vivions en trois lieux: l'école, l'église et la patinoire; mais la vraie vie était sur la patinoire.", + "valid": true + }, + { + "description": "unicode character in hex format in string", + "data": "Les hivers de mon enfance étaient des saisons longues, longues. Nous vivions en trois lieux: l'\u00e9cole, l'église et la patinoire; mais la vraie vie était sur la patinoire.", + "valid": true + }, + { + "description": "unicode matching is case-sensitive", + "data": "LES HIVERS DE MON ENFANCE ÉTAIENT DES SAISONS LONGUES, LONGUES. NOUS VIVIONS EN TROIS LIEUX: L'ÉCOLE, L'ÉGLISE ET LA PATINOIRE; MAIS LA VRAIE VIE ÉTAIT SUR LA PATINOIRE.", + "valid": false + } + ] + }, + { + "description": "\\w in patterns matches [A-Za-z0-9_], not unicode letters", + "schema": { "pattern": "\\wcole" }, + "tests": [ + { + "description": "ascii character in json string", + "data": "Les hivers de mon enfance etaient des saisons longues, longues. Nous vivions en trois lieux: l'ecole, l'eglise et la patinoire; mais la vraie vie etait sur la patinoire.", + "valid": true + }, + { + "description": "literal unicode character in json string", + "data": "Les hivers de mon enfance étaient des saisons longues, longues. Nous vivions en trois lieux: l'école, l'église et la patinoire; mais la vraie vie était sur la patinoire.", + "valid": false + }, + { + "description": "unicode character in hex format in string", + "data": "Les hivers de mon enfance étaient des saisons longues, longues. Nous vivions en trois lieux: l'\u00e9cole, l'église et la patinoire; mais la vraie vie était sur la patinoire.", + "valid": false + }, + { + "description": "unicode matching is case-sensitive", + "data": "LES HIVERS DE MON ENFANCE ÉTAIENT DES SAISONS LONGUES, LONGUES. NOUS VIVIONS EN TROIS LIEUX: L'ÉCOLE, L'ÉGLISE ET LA PATINOIRE; MAIS LA VRAIE VIE ÉTAIT SUR LA PATINOIRE.", + "valid": false + } + ] + }, + { + "description": "unicode characters do not match ascii ranges", + "schema": { "pattern": "[a-z]cole" }, + "tests": [ + { + "description": "literal unicode character in json string", + "data": "Les hivers de mon enfance étaient des saisons longues, longues. Nous vivions en trois lieux: l'école, l'église et la patinoire; mais la vraie vie était sur la patinoire.", + "valid": false + }, + { + "description": "unicode character in hex format in string", + "data": "Les hivers de mon enfance étaient des saisons longues, longues. Nous vivions en trois lieux: l'\u00e9cole, l'église et la patinoire; mais la vraie vie était sur la patinoire.", + "valid": false + }, + { + "description": "ascii characters match", + "data": "Les hivers de mon enfance etaient des saisons longues, longues. Nous vivions en trois lieux: l'ecole, l'eglise et la patinoire; mais la vraie vie etait sur la patinoire.", + "valid": true + } + ] + }, + { + "description": "\\d in pattern matches [0-9], not unicode digits", + "schema": { "pattern": "^\\d+$" }, + "tests": [ + { + "description": "ascii digits", + "data": "42", + "valid": true + }, + { + "description": "ascii non-digits", + "data": "-%#", + "valid": false + }, + { + "description": "non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO)", + "data": "৪২", + "valid": false + } + ] + }, + { + "description": "unicode digits are more than 0 through 9", + "schema": { "pattern": "^\\p{digit}+$" }, + "tests": [ + { + "description": "ascii digits", + "data": "42", + "valid": true + }, + { + "description": "ascii non-digits", + "data": "-%#", + "valid": false + }, + { + "description": "non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO)", + "data": "৪২", + "valid": true + } + ] + }, + { + "description": "unicode semantics should be used for all patternProperties matching", + "schema": { + "type": "object", + "patternProperties": { + "\\p{Letter}cole": true + }, + "additionalProperties": false + }, + "tests": [ + { + "description": "ascii character in json string", + "data": { "l'ecole": "pas de vraie vie" }, + "valid": true + }, + { + "description": "literal unicode character in json string", + "data": { "l'école": "pas de vraie vie" }, + "valid": true + }, + { + "description": "unicode character in hex format in string", + "data": { "l'\u00e9cole": "pas de vraie vie" }, + "valid": true + }, + { + "description": "unicode matching is case-sensitive", + "data": { "L'ÉCOLE": "PAS DE VRAIE VIE" }, + "valid": false + } + ] + }, + { + "description": "\\w in patternProperties matches [A-Za-z0-9_], not unicode letters", + "schema": { + "type": "object", + "patternProperties": { + "\\wcole": true + }, + "additionalProperties": false + }, + "tests": [ + { + "description": "ascii character in json string", + "data": { "l'ecole": "pas de vraie vie" }, + "valid": true + }, + { + "description": "literal unicode character in json string", + "data": { "l'école": "pas de vraie vie" }, + "valid": false + }, + { + "description": "unicode character in hex format in string", + "data": { "l'\u00e9cole": "pas de vraie vie" }, + "valid": false + }, + { + "description": "unicode matching is case-sensitive", + "data": { "L'ÉCOLE": "PAS DE VRAIE VIE" }, + "valid": false + } + ] + }, + { + "description": "unicode characters do not match ascii ranges", + "schema": { + "type": "object", + "patternProperties": { + "[a-z]cole": true + }, + "additionalProperties": false + }, + "tests": [ + { + "description": "literal unicode character in json string", + "data": { "l'école": "pas de vraie vie" }, + "valid": false + }, + { + "description": "unicode character in hex format in string", + "data": { "l'\u00e9cole": "pas de vraie vie" }, + "valid": false + }, + { + "description": "ascii characters match", + "data": { "l'ecole": "pas de vraie vie" }, + "valid": true + } + ] + }, + { + "description": "\\d in patternProperties matches [0-9], not unicode digits", + "schema": { + "type": "object", + "patternProperties": { + "^\\d+$": true + }, + "additionalProperties": false + }, + "tests": [ + { + "description": "ascii digits", + "data": { "42": "life, the universe, and everything" }, + "valid": true + }, + { + "description": "ascii non-digits", + "data": { "-%#": "spending the year dead for tax reasons" }, + "valid": false + }, + { + "description": "non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO)", + "data": { "৪২": "khajit has wares if you have coin" }, + "valid": false + } + ] + }, + { + "description": "unicode digits are more than 0 through 9", + "schema": { + "type": "object", + "patternProperties": { + "^\\p{digit}+$": true + }, + "additionalProperties": false + }, + "tests": [ + { + "description": "ascii digits", + "data": { "42": "life, the universe, and everything" }, + "valid": true + }, + { + "description": "ascii non-digits", + "data": { "-%#": "spending the year dead for tax reasons" }, + "valid": false + }, + { + "description": "non-ascii digits (BENGALI DIGIT FOUR, BENGALI DIGIT TWO)", + "data": { "৪২": "khajit has wares if you have coin" }, + "valid": true + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/float-overflow.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/float-overflow.json new file mode 100755 index 000000000000..52ff9827cf7b --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/float-overflow.json @@ -0,0 +1,13 @@ +[ + { + "description": "all integers are multiples of 0.5, if overflow is handled", + "schema": {"type": "integer", "multipleOf": 0.5}, + "tests": [ + { + "description": "valid if optional overflow handling is implemented", + "data": 1e308, + "valid": true + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/date-time.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/date-time.json new file mode 100755 index 000000000000..f4f993355580 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/date-time.json @@ -0,0 +1,133 @@ +[ + { + "description": "validation of date-time strings", + "schema": { "format": "date-time" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + }, + { + "description": "a valid date-time string", + "data": "1963-06-19T08:30:06.283185Z", + "valid": true + }, + { + "description": "a valid date-time string without second fraction", + "data": "1963-06-19T08:30:06Z", + "valid": true + }, + { + "description": "a valid date-time string with plus offset", + "data": "1937-01-01T12:00:27.87+00:20", + "valid": true + }, + { + "description": "a valid date-time string with minus offset", + "data": "1990-12-31T15:59:50.123-08:00", + "valid": true + }, + { + "description": "a valid date-time with a leap second, UTC", + "data": "1998-12-31T23:59:60Z", + "valid": true + }, + { + "description": "a valid date-time with a leap second, with minus offset", + "data": "1998-12-31T15:59:60.123-08:00", + "valid": true + }, + { + "description": "an invalid date-time past leap second, UTC", + "data": "1998-12-31T23:59:61Z", + "valid": false + }, + { + "description": "an invalid date-time with leap second on a wrong minute, UTC", + "data": "1998-12-31T23:58:60Z", + "valid": false + }, + { + "description": "an invalid date-time with leap second on a wrong hour, UTC", + "data": "1998-12-31T22:59:60Z", + "valid": false + }, + { + "description": "an invalid day in date-time string", + "data": "1990-02-31T15:59:59.123-08:00", + "valid": false + }, + { + "description": "an invalid offset in date-time string", + "data": "1990-12-31T15:59:59-24:00", + "valid": false + }, + { + "description": "an invalid closing Z after time-zone offset", + "data": "1963-06-19T08:30:06.28123+01:00Z", + "valid": false + }, + { + "description": "an invalid date-time string", + "data": "06/19/1963 08:30:06 PST", + "valid": false + }, + { + "description": "case-insensitive T and Z", + "data": "1963-06-19t08:30:06.283185z", + "valid": true + }, + { + "description": "only RFC3339 not all of ISO 8601 are valid", + "data": "2013-350T01:01:01", + "valid": false + }, + { + "description": "invalid non-padded month dates", + "data": "1963-6-19T08:30:06.283185Z", + "valid": false + }, + { + "description": "invalid non-padded day dates", + "data": "1963-06-1T08:30:06.283185Z", + "valid": false + }, + { + "description": "non-ascii digits should be rejected in the date portion", + "data": "1963-06-1৪T00:00:00Z", + "valid": false + }, + { + "description": "non-ascii digits should be rejected in the time portion", + "data": "1963-06-11T0৪:00:00Z", + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/email.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/email.json new file mode 100755 index 000000000000..d6761a46babc --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/email.json @@ -0,0 +1,83 @@ +[ + { + "description": "validation of e-mail addresses", + "schema": { "format": "email" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + }, + { + "description": "a valid e-mail address", + "data": "joe.bloggs@example.com", + "valid": true + }, + { + "description": "an invalid e-mail address", + "data": "2962", + "valid": false + }, + { + "description": "tilde in local part is valid", + "data": "te~st@example.com", + "valid": true + }, + { + "description": "tilde before local part is valid", + "data": "~test@example.com", + "valid": true + }, + { + "description": "tilde after local part is valid", + "data": "test~@example.com", + "valid": true + }, + { + "description": "dot before local part is not valid", + "data": ".test@example.com", + "valid": false + }, + { + "description": "dot after local part is not valid", + "data": "test.@example.com", + "valid": false + }, + { + "description": "two separated dots inside local part are valid", + "data": "te.s.t@example.com", + "valid": true + }, + { + "description": "two subsequent dots inside local part are not valid", + "data": "te..st@example.com", + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/hostname.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/hostname.json new file mode 100755 index 000000000000..8a67fda8807c --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/hostname.json @@ -0,0 +1,98 @@ +[ + { + "description": "validation of host names", + "schema": { "format": "hostname" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + }, + { + "description": "a valid host name", + "data": "www.example.com", + "valid": true + }, + { + "description": "a valid punycoded IDN hostname", + "data": "xn--4gbwdl.xn--wgbh1c", + "valid": true + }, + { + "description": "a host name starting with an illegal character", + "data": "-a-host-name-that-starts-with--", + "valid": false + }, + { + "description": "a host name containing illegal characters", + "data": "not_a_valid_host_name", + "valid": false + }, + { + "description": "a host name with a component too long", + "data": "a-vvvvvvvvvvvvvvvveeeeeeeeeeeeeeeerrrrrrrrrrrrrrrryyyyyyyyyyyyyyyy-long-host-name-component", + "valid": false + }, + { + "description": "starts with hyphen", + "data": "-hostname", + "valid": false + }, + { + "description": "ends with hyphen", + "data": "hostname-", + "valid": false + }, + { + "description": "starts with underscore", + "data": "_hostname", + "valid": false + }, + { + "description": "ends with underscore", + "data": "hostname_", + "valid": false + }, + { + "description": "contains underscore", + "data": "host_name", + "valid": false + }, + { + "description": "maximum label length", + "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijk.com", + "valid": true + }, + { + "description": "exceeds maximum label length", + "data": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl.com", + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/ipv4.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/ipv4.json new file mode 100755 index 000000000000..6b166c70cda1 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/ipv4.json @@ -0,0 +1,84 @@ +[ + { + "description": "validation of IP addresses", + "schema": { "format": "ipv4" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + }, + { + "description": "a valid IP address", + "data": "192.168.0.1", + "valid": true + }, + { + "description": "an IP address with too many components", + "data": "127.0.0.0.1", + "valid": false + }, + { + "description": "an IP address with out-of-range values", + "data": "256.256.256.256", + "valid": false + }, + { + "description": "an IP address without 4 components", + "data": "127.0", + "valid": false + }, + { + "description": "an IP address as an integer", + "data": "0x7f000001", + "valid": false + }, + { + "description": "an IP address as an integer (decimal)", + "data": "2130706433", + "valid": false + }, + { + "description": "leading zeroes should be rejected, as they are treated as octals", + "comment": "see https://sick.codes/universal-netmask-npm-package-used-by-270000-projects-vulnerable-to-octal-input-data-server-side-request-forgery-remote-file-inclusion-local-file-inclusion-and-more-cve-2021-28918/", + "data": "087.10.0.1", + "valid": false + }, + { + "description": "value without leading zero is valid", + "data": "87.10.0.1", + "valid": true + }, + { + "description": "non-ascii digits should be rejected", + "data": "1২7.0.0.1", + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/ipv6.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/ipv6.json new file mode 100755 index 000000000000..6379927b3e85 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/ipv6.json @@ -0,0 +1,208 @@ +[ + { + "description": "validation of IPv6 addresses", + "schema": { "format": "ipv6" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + }, + { + "description": "a valid IPv6 address", + "data": "::1", + "valid": true + }, + { + "description": "an IPv6 address with out-of-range values", + "data": "12345::", + "valid": false + }, + { + "description": "trailing 4 hex symbols is valid", + "data": "::abef", + "valid": true + }, + { + "description": "trailing 5 hex symbols is invalid", + "data": "::abcef", + "valid": false + }, + { + "description": "an IPv6 address with too many components", + "data": "1:1:1:1:1:1:1:1:1:1:1:1:1:1:1:1", + "valid": false + }, + { + "description": "an IPv6 address containing illegal characters", + "data": "::laptop", + "valid": false + }, + { + "description": "no digits is valid", + "data": "::", + "valid": true + }, + { + "description": "leading colons is valid", + "data": "::42:ff:1", + "valid": true + }, + { + "description": "trailing colons is valid", + "data": "d6::", + "valid": true + }, + { + "description": "missing leading octet is invalid", + "data": ":2:3:4:5:6:7:8", + "valid": false + }, + { + "description": "missing trailing octet is invalid", + "data": "1:2:3:4:5:6:7:", + "valid": false + }, + { + "description": "missing leading octet with omitted octets later", + "data": ":2:3:4::8", + "valid": false + }, + { + "description": "single set of double colons in the middle is valid", + "data": "1:d6::42", + "valid": true + }, + { + "description": "two sets of double colons is invalid", + "data": "1::d6::42", + "valid": false + }, + { + "description": "mixed format with the ipv4 section as decimal octets", + "data": "1::d6:192.168.0.1", + "valid": true + }, + { + "description": "mixed format with double colons between the sections", + "data": "1:2::192.168.0.1", + "valid": true + }, + { + "description": "mixed format with ipv4 section with octet out of range", + "data": "1::2:192.168.256.1", + "valid": false + }, + { + "description": "mixed format with ipv4 section with a hex octet", + "data": "1::2:192.168.ff.1", + "valid": false + }, + { + "description": "mixed format with leading double colons (ipv4-mapped ipv6 address)", + "data": "::ffff:192.168.0.1", + "valid": true + }, + { + "description": "triple colons is invalid", + "data": "1:2:3:4:5:::8", + "valid": false + }, + { + "description": "8 octets", + "data": "1:2:3:4:5:6:7:8", + "valid": true + }, + { + "description": "insufficient octets without double colons", + "data": "1:2:3:4:5:6:7", + "valid": false + }, + { + "description": "no colons is invalid", + "data": "1", + "valid": false + }, + { + "description": "ipv4 is not ipv6", + "data": "127.0.0.1", + "valid": false + }, + { + "description": "ipv4 segment must have 4 octets", + "data": "1:2:3:4:1.2.3", + "valid": false + }, + { + "description": "leading whitespace is invalid", + "data": " ::1", + "valid": false + }, + { + "description": "trailing whitespace is invalid", + "data": "::1 ", + "valid": false + }, + { + "description": "netmask is not a part of ipv6 address", + "data": "fe80::/64", + "valid": false + }, + { + "description": "zone id is not a part of ipv6 address", + "data": "fe80::a%eth1", + "valid": false + }, + { + "description": "a long valid ipv6", + "data": "1000:1000:1000:1000:1000:1000:255.255.255.255", + "valid": true + }, + { + "description": "a long invalid ipv6, below length limit, first", + "data": "100:100:100:100:100:100:255.255.255.255.255", + "valid": false + }, + { + "description": "a long invalid ipv6, below length limit, second", + "data": "100:100:100:100:100:100:100:255.255.255.255", + "valid": false + }, + { + "description": "non-ascii digits should be rejected", + "data": "1:2:3:4:5:6:7:৪", + "valid": false + }, + { + "description": "non-ascii digits should be rejected in the ipv4 portion also", + "data": "1:2::192.16৪.0.1", + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/json-pointer.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/json-pointer.json new file mode 100755 index 000000000000..a0346b575227 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/json-pointer.json @@ -0,0 +1,198 @@ +[ + { + "description": "validation of JSON-pointers (JSON String Representation)", + "schema": { "format": "json-pointer" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + }, + { + "description": "a valid JSON-pointer", + "data": "/foo/bar~0/baz~1/%a", + "valid": true + }, + { + "description": "not a valid JSON-pointer (~ not escaped)", + "data": "/foo/bar~", + "valid": false + }, + { + "description": "valid JSON-pointer with empty segment", + "data": "/foo//bar", + "valid": true + }, + { + "description": "valid JSON-pointer with the last empty segment", + "data": "/foo/bar/", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #1", + "data": "", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #2", + "data": "/foo", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #3", + "data": "/foo/0", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #4", + "data": "/", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #5", + "data": "/a~1b", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #6", + "data": "/c%d", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #7", + "data": "/e^f", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #8", + "data": "/g|h", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #9", + "data": "/i\\j", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #10", + "data": "/k\"l", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #11", + "data": "/ ", + "valid": true + }, + { + "description": "valid JSON-pointer as stated in RFC 6901 #12", + "data": "/m~0n", + "valid": true + }, + { + "description": "valid JSON-pointer used adding to the last array position", + "data": "/foo/-", + "valid": true + }, + { + "description": "valid JSON-pointer (- used as object member name)", + "data": "/foo/-/bar", + "valid": true + }, + { + "description": "valid JSON-pointer (multiple escaped characters)", + "data": "/~1~0~0~1~1", + "valid": true + }, + { + "description": "valid JSON-pointer (escaped with fraction part) #1", + "data": "/~1.1", + "valid": true + }, + { + "description": "valid JSON-pointer (escaped with fraction part) #2", + "data": "/~0.1", + "valid": true + }, + { + "description": "not a valid JSON-pointer (URI Fragment Identifier) #1", + "data": "#", + "valid": false + }, + { + "description": "not a valid JSON-pointer (URI Fragment Identifier) #2", + "data": "#/", + "valid": false + }, + { + "description": "not a valid JSON-pointer (URI Fragment Identifier) #3", + "data": "#a", + "valid": false + }, + { + "description": "not a valid JSON-pointer (some escaped, but not all) #1", + "data": "/~0~", + "valid": false + }, + { + "description": "not a valid JSON-pointer (some escaped, but not all) #2", + "data": "/~0/~", + "valid": false + }, + { + "description": "not a valid JSON-pointer (wrong escape character) #1", + "data": "/~2", + "valid": false + }, + { + "description": "not a valid JSON-pointer (wrong escape character) #2", + "data": "/~-1", + "valid": false + }, + { + "description": "not a valid JSON-pointer (multiple characters not escaped)", + "data": "/~~", + "valid": false + }, + { + "description": "not a valid JSON-pointer (isn't empty nor starts with /) #1", + "data": "a", + "valid": false + }, + { + "description": "not a valid JSON-pointer (isn't empty nor starts with /) #2", + "data": "0", + "valid": false + }, + { + "description": "not a valid JSON-pointer (isn't empty nor starts with /) #3", + "data": "a/a", + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/unknown.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/unknown.json new file mode 100755 index 000000000000..12339ae57dfa --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/unknown.json @@ -0,0 +1,43 @@ +[ + { + "description": "unknown format", + "schema": { "format": "unknown" }, + "tests": [ + { + "description": "unknown formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "unknown formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "unknown formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "unknown formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "unknown formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "unknown formats ignore nulls", + "data": null, + "valid": true + }, + { + "description": "unknown formats ignore strings", + "data": "string", + "valid": true + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/uri-reference.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/uri-reference.json new file mode 100755 index 000000000000..7cdf228d8b18 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/uri-reference.json @@ -0,0 +1,73 @@ +[ + { + "description": "validation of URI References", + "schema": { "format": "uri-reference" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + }, + { + "description": "a valid URI", + "data": "http://foo.bar/?baz=qux#quux", + "valid": true + }, + { + "description": "a valid protocol-relative URI Reference", + "data": "//foo.bar/?baz=qux#quux", + "valid": true + }, + { + "description": "a valid relative URI Reference", + "data": "/abc", + "valid": true + }, + { + "description": "an invalid URI Reference", + "data": "\\\\WINDOWS\\fileshare", + "valid": false + }, + { + "description": "a valid URI Reference", + "data": "abc", + "valid": true + }, + { + "description": "a valid URI fragment", + "data": "#fragment", + "valid": true + }, + { + "description": "an invalid URI fragment", + "data": "#frag\\ment", + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/uri-template.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/uri-template.json new file mode 100755 index 000000000000..df355c55aa3f --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/uri-template.json @@ -0,0 +1,58 @@ +[ + { + "description": "format: uri-template", + "schema": { "format": "uri-template" }, + "tests": [ + { + "description": "all string formats ignore integers", + "data": 12, + "valid": true + }, + { + "description": "all string formats ignore floats", + "data": 13.7, + "valid": true + }, + { + "description": "all string formats ignore objects", + "data": {}, + "valid": true + }, + { + "description": "all string formats ignore arrays", + "data": [], + "valid": true + }, + { + "description": "all string formats ignore booleans", + "data": false, + "valid": true + }, + { + "description": "all string formats ignore nulls", + "data": null, + "valid": true + }, + { + "description": "a valid uri-template", + "data": "http://example.com/dictionary/{term:1}/{term}", + "valid": true + }, + { + "description": "an invalid uri-template", + "data": "http://example.com/dictionary/{term:1}/{term", + "valid": false + }, + { + "description": "a valid uri-template without variables", + "data": "http://example.com/dictionary", + "valid": true + }, + { + "description": "a valid relative uri-template", + "data": "dictionary/{term:1}/{term}", + "valid": true + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/uri.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/uri.json new file mode 100755 index 000000000000..792d71a05836 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/format/uri.json @@ -0,0 +1,108 @@ +[ + { + "description": "validation of URIs", + "schema": { "format": "uri" }, + "tests": [ + { + "description": "a valid URL with anchor tag", + "data": "http://foo.bar/?baz=qux#quux", + "valid": true + }, + { + "description": "a valid URL with anchor tag and parentheses", + "data": "http://foo.com/blah_(wikipedia)_blah#cite-1", + "valid": true + }, + { + "description": "a valid URL with URL-encoded stuff", + "data": "http://foo.bar/?q=Test%20URL-encoded%20stuff", + "valid": true + }, + { + "description": "a valid puny-coded URL ", + "data": "http://xn--nw2a.xn--j6w193g/", + "valid": true + }, + { + "description": "a valid URL with many special characters", + "data": "http://-.~_!$&'()*+,;=:%40:80%2f::::::@example.com", + "valid": true + }, + { + "description": "a valid URL based on IPv4", + "data": "http://223.255.255.254", + "valid": true + }, + { + "description": "a valid URL with ftp scheme", + "data": "ftp://ftp.is.co.za/rfc/rfc1808.txt", + "valid": true + }, + { + "description": "a valid URL for a simple text file", + "data": "http://www.ietf.org/rfc/rfc2396.txt", + "valid": true + }, + { + "description": "a valid URL ", + "data": "ldap://[2001:db8::7]/c=GB?objectClass?one", + "valid": true + }, + { + "description": "a valid mailto URI", + "data": "mailto:John.Doe@example.com", + "valid": true + }, + { + "description": "a valid newsgroup URI", + "data": "news:comp.infosystems.www.servers.unix", + "valid": true + }, + { + "description": "a valid tel URI", + "data": "tel:+1-816-555-1212", + "valid": true + }, + { + "description": "a valid URN", + "data": "urn:oasis:names:specification:docbook:dtd:xml:4.1.2", + "valid": true + }, + { + "description": "an invalid protocol-relative URI Reference", + "data": "//foo.bar/?baz=qux#quux", + "valid": false + }, + { + "description": "an invalid relative URI Reference", + "data": "/abc", + "valid": false + }, + { + "description": "an invalid URI", + "data": "\\\\WINDOWS\\fileshare", + "valid": false + }, + { + "description": "an invalid URI though valid URI reference", + "data": "abc", + "valid": false + }, + { + "description": "an invalid URI with spaces", + "data": "http:// shouldfail.com", + "valid": false + }, + { + "description": "an invalid URI with spaces and missing scheme", + "data": ":// should fail", + "valid": false + }, + { + "description": "an invalid URI with comma in scheme", + "data": "bar,baz:foo", + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/future-keywords.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/future-keywords.json new file mode 100755 index 000000000000..af2f791321aa --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/future-keywords.json @@ -0,0 +1,465 @@ +[ + { + "description": "$dynamicRef without $dynamicAnchor works like $ref", + "schema": { + "properties": { + "foo": {"$dynamicRef": "#"} + }, + "additionalProperties": false + }, + "tests": [ + { + "description": "match", + "data": {"foo": false}, + "valid": true + }, + { + "description": "recursive match", + "data": {"foo": {"foo": false}}, + "valid": true + }, + { + "description": "mismatch", + "data": {"bar": false}, + "valid": false + }, + { + "description": "recursive mismatch (but $dynamicRef is ignored)", + "data": {"foo": {"bar": false}}, + "valid": true + } + ] + }, + { + "description": "prefixItems: an array of schemas for items", + "schema": { + "prefixItems": [ + {"type": "integer"}, + {"type": "string"} + ] + }, + "tests": [ + { + "description": "correct types", + "data": [ 1, "foo" ], + "valid": true + }, + { + "description": "wrong types", + "data": [ "foo", 1 ], + "valid": true + }, + { + "description": "incomplete array of items", + "data": [ 1 ], + "valid": true + }, + { + "description": "array with additional items", + "data": [ 1, "foo", true ], + "valid": true + }, + { + "description": "empty array", + "data": [ ], + "valid": true + }, + { + "description": "JavaScript pseudo-array is valid", + "data": { + "0": "invalid", + "1": "valid", + "length": 2 + }, + "valid": true + } + ] + }, + { + "description": "dependentSchemas: single dependency", + "schema": { + "dependentSchemas": { + "bar": { + "properties": { + "foo": {"type": "integer"}, + "bar": {"type": "integer"} + } + } + } + }, + "tests": [ + { + "description": "valid", + "data": {"foo": 1, "bar": 2}, + "valid": true + }, + { + "description": "no dependency", + "data": {"foo": "quux"}, + "valid": true + }, + { + "description": "wrong type", + "data": {"foo": "quux", "bar": 2}, + "valid": true + }, + { + "description": "wrong type other", + "data": {"foo": 2, "bar": "quux"}, + "valid": true + }, + { + "description": "wrong type both", + "data": {"foo": "quux", "bar": "quux"}, + "valid": true + } + ] + }, + { + "description": "dependentRequired: single dependency", + "schema": {"dependentRequired": {"bar": ["foo"]}}, + "tests": [ + { + "description": "neither", + "data": {}, + "valid": true + }, + { + "description": "nondependant", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "with dependency", + "data": {"foo": 1, "bar": 2}, + "valid": true + }, + { + "description": "missing dependency", + "data": {"bar": 2}, + "valid": true + }, + { + "description": "ignores arrays", + "data": ["bar"], + "valid": true + }, + { + "description": "ignores strings", + "data": "foobar", + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + } + ] + }, + { + "description": "unevaluatedItems false", + "schema": { + "type": "array", + "unevaluatedItems": false + }, + "tests": [ + { + "description": "with no unevaluated items", + "data": [], + "valid": true + }, + { + "description": "with unevaluated items", + "data": ["foo"], + "valid": true + } + ] + }, + { + "description": "unevaluatedProperties schema", + "schema": { + "type": "object", + "unevaluatedProperties": { + "type": "string", + "minLength": 3 + } + }, + "tests": [ + { + "description": "with no unevaluated properties", + "data": {}, + "valid": true + }, + { + "description": "with valid unevaluated properties", + "data": { + "foo": "foo" + }, + "valid": true + }, + { + "description": "with invalid unevaluated properties", + "data": { + "foo": "fo" + }, + "valid": true + } + ] + }, + { + "description": "maxContains with contains", + "schema": { + "contains": {"const": 1}, + "maxContains": 1 + }, + "tests": [ + { + "description": "empty data", + "data": [ ], + "valid": false + }, + { + "description": "all elements match, valid maxContains", + "data": [ 1 ], + "valid": true + }, + { + "description": "all elements match, invalid maxContains", + "data": [ 1, 1 ], + "valid": true + }, + { + "description": "some elements match, valid maxContains", + "data": [ 1, 2 ], + "valid": true + }, + { + "description": "some elements match, invalid maxContains", + "data": [ 1, 2, 1 ], + "valid": true + } + ] + }, + { + "description": "minContains=2 with contains", + "schema": { + "contains": {"const": 1}, + "minContains": 2 + }, + "tests": [ + { + "description": "empty data", + "data": [ ], + "valid": false + }, + { + "description": "all elements match, invalid minContains", + "data": [ 1 ], + "valid": true + }, + { + "description": "some elements match, invalid minContains", + "data": [ 1, 2 ], + "valid": true + }, + { + "description": "all elements match, valid minContains (exactly as needed)", + "data": [ 1, 1 ], + "valid": true + }, + { + "description": "all elements match, valid minContains (more than needed)", + "data": [ 1, 1, 1 ], + "valid": true + }, + { + "description": "some elements match, valid minContains", + "data": [ 1, 2, 1 ], + "valid": true + } + ] + }, + { + "description": "minContains = 0", + "schema": { + "contains": {"const": 1}, + "minContains": 0 + }, + "tests": [ + { + "description": "empty array is valid with minContains=0", + "data": [ ], + "valid": false + }, + { + "description": "minContains = 0 would make contains always pass", + "data": [ 2 ], + "valid": false + } + ] + }, + { + "description": "if with boolean schema true", + "schema": { "if": true, "then": { "const": "then" }, "else": { "const": "else" } }, + "tests": [ + { + "description": "boolean schema true in if (invalid when supported)", + "data": "then", + "valid": true + }, + { + "description": "boolean schema true in if (valid when supported)", + "data": "else", + "valid": true + } + ] + }, + { + "description": "if with boolean schema false", + "schema": { "if": false, "then": { "const": "then" }, "else": { "const": "else" } }, + "tests": [ + { + "description": "boolean schema false in if (invalid when supported)", + "data": "then", + "valid": true + }, + { + "description": "boolean schema false in if (valid when supported)", + "data": "else", + "valid": true + } + ] + }, + { + "description": "$recursiveRef without $recursiveAnchor works like $ref", + "schema": { + "properties": { + "foo": { "$recursiveRef": "#" } + }, + "additionalProperties": false + }, + "tests": [ + { + "description": "match", + "data": {"foo": false}, + "valid": true + }, + { + "description": "recursive match", + "data": { "foo": { "foo": false } }, + "valid": true + }, + { + "description": "mismatch", + "data": { "bar": false }, + "valid": false + }, + { + "description": "recursive mismatch", + "data": { "foo": { "bar": false } }, + "valid": true + } + ] + }, + { + "description": "$recursiveRef without using nesting", + "schema": { + "$id": "http://localhost:4242", + "definitions": { + "myobject": { + "$id": "myobject.json", + "$recursiveAnchor": true, + "anyOf": [ + { "type": "string" }, + { + "type": "object", + "additionalProperties": { "$recursiveRef": "#" } + } + ] + } + }, + "anyOf": [ + { "type": "integer" }, + { "$ref": "#/definitions/myobject" } + ] + }, + "tests": [ + { + "description": "integer matches at the outer level", + "data": 1, + "valid": true + }, + { + "description": "single level match", + "data": { "foo": "hi" }, + "valid": true + }, + { + "description": "integer does not match as a property value", + "data": { "foo": 1 }, + "valid": true + }, + { + "description": "two levels, additionalProperties always matches, 1", + "data": { "foo": { "bar": "hi" } }, + "valid": true + }, + { + "description": "two levels, additionalProperties always matches, 2", + "data": { "foo": { "bar": 1 } }, + "valid": true + } + ] + }, + { + "description": "$recursiveRef with nesting", + "schema": { + "$id": "http://localhost:4242", + "$recursiveAnchor": true, + "definitions": { + "myobject": { + "$id": "myobject.json", + "$recursiveAnchor": true, + "anyOf": [ + { "type": "string" }, + { + "type": "object", + "additionalProperties": { "$recursiveRef": "#" } + } + ] + } + }, + "anyOf": [ + { "type": "integer" }, + { "$ref": "#/definitions/myobject" } + ] + }, + "tests": [ + { + "description": "integer matches at the outer level", + "data": 1, + "valid": true + }, + { + "description": "single level match", + "data": { "foo": "hi" }, + "valid": true + }, + { + "description": "integer now matches as a property value", + "data": { "foo": 1 }, + "valid": true + }, + { + "description": "two levels, properties match with inner definition", + "data": { "foo": { "bar": "hi" } }, + "valid": true + }, + { + "description": "two levels, properties match with $recursiveRef", + "data": { "foo": { "bar": 1 } }, + "valid": true + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/non-bmp-regex.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/non-bmp-regex.json new file mode 100755 index 000000000000..dd67af2b2ada --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/optional/non-bmp-regex.json @@ -0,0 +1,82 @@ +[ + { + "description": "Proper UTF-16 surrogate pair handling: pattern", + "comment": "Optional because .Net doesn't correctly handle 32-bit Unicode characters", + "schema": { "pattern": "^🐲*$" }, + "tests": [ + { + "description": "matches empty", + "data": "", + "valid": true + }, + { + "description": "matches single", + "data": "🐲", + "valid": true + }, + { + "description": "matches two", + "data": "🐲🐲", + "valid": true + }, + { + "description": "doesn't match one", + "data": "🐉", + "valid": false + }, + { + "description": "doesn't match two", + "data": "🐉🐉", + "valid": false + }, + { + "description": "doesn't match one ASCII", + "data": "D", + "valid": false + }, + { + "description": "doesn't match two ASCII", + "data": "DD", + "valid": false + } + ] + }, + { + "description": "Proper UTF-16 surrogate pair handling: patternProperties", + "comment": "Optional because .Net doesn't correctly handle 32-bit Unicode characters", + "schema": { + "patternProperties": { + "^🐲*$": { + "type": "integer" + } + } + }, + "tests": [ + { + "description": "matches empty", + "data": { "": 1 }, + "valid": true + }, + { + "description": "matches single", + "data": { "🐲": 1 }, + "valid": true + }, + { + "description": "matches two", + "data": { "🐲🐲": 1 }, + "valid": true + }, + { + "description": "doesn't match one", + "data": { "🐲": "hello" }, + "valid": false + }, + { + "description": "doesn't match two", + "data": { "🐲🐲": "hello" }, + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/pattern.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/pattern.json new file mode 100755 index 000000000000..92db0f971c75 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/pattern.json @@ -0,0 +1,59 @@ +[ + { + "description": "pattern validation", + "schema": {"pattern": "^a*$"}, + "tests": [ + { + "description": "a matching pattern is valid", + "data": "aaa", + "valid": true + }, + { + "description": "a non-matching pattern is invalid", + "data": "abc", + "valid": false + }, + { + "description": "ignores booleans", + "data": true, + "valid": true + }, + { + "description": "ignores integers", + "data": 123, + "valid": true + }, + { + "description": "ignores floats", + "data": 1.0, + "valid": true + }, + { + "description": "ignores objects", + "data": {}, + "valid": true + }, + { + "description": "ignores arrays", + "data": [], + "valid": true + }, + { + "description": "ignores null", + "data": null, + "valid": true + } + ] + }, + { + "description": "pattern is not anchored", + "schema": {"pattern": "a+"}, + "tests": [ + { + "description": "matches a substring", + "data": "xxaayy", + "valid": true + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/patternProperties.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/patternProperties.json new file mode 100755 index 000000000000..c10ffcc05dac --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/patternProperties.json @@ -0,0 +1,156 @@ +[ + { + "description": + "patternProperties validates properties matching a regex", + "schema": { + "patternProperties": { + "f.*o": {"type": "integer"} + } + }, + "tests": [ + { + "description": "a single valid match is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "multiple valid matches is valid", + "data": {"foo": 1, "foooooo" : 2}, + "valid": true + }, + { + "description": "a single invalid match is invalid", + "data": {"foo": "bar", "fooooo": 2}, + "valid": false + }, + { + "description": "multiple invalid matches is invalid", + "data": {"foo": "bar", "foooooo" : "baz"}, + "valid": false + }, + { + "description": "ignores arrays", + "data": ["foo"], + "valid": true + }, + { + "description": "ignores strings", + "data": "foo", + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + } + ] + }, + { + "description": "multiple simultaneous patternProperties are validated", + "schema": { + "patternProperties": { + "a*": {"type": "integer"}, + "aaa*": {"maximum": 20} + } + }, + "tests": [ + { + "description": "a single valid match is valid", + "data": {"a": 21}, + "valid": true + }, + { + "description": "a simultaneous match is valid", + "data": {"aaaa": 18}, + "valid": true + }, + { + "description": "multiple matches is valid", + "data": {"a": 21, "aaaa": 18}, + "valid": true + }, + { + "description": "an invalid due to one is invalid", + "data": {"a": "bar"}, + "valid": false + }, + { + "description": "an invalid due to the other is invalid", + "data": {"aaaa": 31}, + "valid": false + }, + { + "description": "an invalid due to both is invalid", + "data": {"aaa": "foo", "aaaa": 31}, + "valid": false + } + ] + }, + { + "description": "regexes are not anchored by default and are case sensitive", + "schema": { + "patternProperties": { + "[0-9]{2,}": { "type": "boolean" }, + "X_": { "type": "string" } + } + }, + "tests": [ + { + "description": "non recognized members are ignored", + "data": { "answer 1": "42" }, + "valid": true + }, + { + "description": "recognized members are accounted for", + "data": { "a31b": null }, + "valid": false + }, + { + "description": "regexes are case sensitive", + "data": { "a_x_3": 3 }, + "valid": true + }, + { + "description": "regexes are case sensitive, 2", + "data": { "a_X_3": 3 }, + "valid": false + } + ] + }, + { + "description": "patternProperties with boolean schemas", + "schema": { + "patternProperties": { + "f.*": true, + "b.*": false + } + }, + "tests": [ + { + "description": "object with property matching schema true is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "object with property matching schema false is invalid", + "data": {"bar": 2}, + "valid": false + }, + { + "description": "object with both properties is invalid", + "data": {"foo": 1, "bar": 2}, + "valid": false + }, + { + "description": "object with a property matching both true and false is invalid", + "data": {"foobar":1}, + "valid": false + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/properties.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/properties.json new file mode 100755 index 000000000000..b86c18198289 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/properties.json @@ -0,0 +1,167 @@ +[ + { + "description": "object properties validation", + "schema": { + "properties": { + "foo": {"type": "integer"}, + "bar": {"type": "string"} + } + }, + "tests": [ + { + "description": "both properties present and valid is valid", + "data": {"foo": 1, "bar": "baz"}, + "valid": true + }, + { + "description": "one property invalid is invalid", + "data": {"foo": 1, "bar": {}}, + "valid": false + }, + { + "description": "both properties invalid is invalid", + "data": {"foo": [], "bar": {}}, + "valid": false + }, + { + "description": "doesn't invalidate other properties", + "data": {"quux": []}, + "valid": true + }, + { + "description": "ignores arrays", + "data": [], + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + } + ] + }, + { + "description": + "properties, patternProperties, additionalProperties interaction", + "schema": { + "properties": { + "foo": {"type": "array", "maxItems": 3}, + "bar": {"type": "array"} + }, + "patternProperties": {"f.o": {"minItems": 2}}, + "additionalProperties": {"type": "integer"} + }, + "tests": [ + { + "description": "property validates property", + "data": {"foo": [1, 2]}, + "valid": true + }, + { + "description": "property invalidates property", + "data": {"foo": [1, 2, 3, 4]}, + "valid": false + }, + { + "description": "patternProperty invalidates property", + "data": {"foo": []}, + "valid": false + }, + { + "description": "patternProperty validates nonproperty", + "data": {"fxo": [1, 2]}, + "valid": true + }, + { + "description": "patternProperty invalidates nonproperty", + "data": {"fxo": []}, + "valid": false + }, + { + "description": "additionalProperty ignores property", + "data": {"bar": []}, + "valid": true + }, + { + "description": "additionalProperty validates others", + "data": {"quux": 3}, + "valid": true + }, + { + "description": "additionalProperty invalidates others", + "data": {"quux": "foo"}, + "valid": false + } + ] + }, + { + "description": "properties with boolean schema", + "schema": { + "properties": { + "foo": true, + "bar": false + } + }, + "tests": [ + { + "description": "no property present is valid", + "data": {}, + "valid": true + }, + { + "description": "only 'true' property present is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "only 'false' property present is invalid", + "data": {"bar": 2}, + "valid": false + }, + { + "description": "both properties present is invalid", + "data": {"foo": 1, "bar": 2}, + "valid": false + } + ] + }, + { + "description": "properties with escaped characters", + "schema": { + "properties": { + "foo\nbar": {"type": "number"}, + "foo\"bar": {"type": "number"}, + "foo\\bar": {"type": "number"}, + "foo\rbar": {"type": "number"}, + "foo\tbar": {"type": "number"}, + "foo\fbar": {"type": "number"} + } + }, + "tests": [ + { + "description": "object with all numbers is valid", + "data": { + "foo\nbar": 1, + "foo\"bar": 1, + "foo\\bar": 1, + "foo\rbar": 1, + "foo\tbar": 1, + "foo\fbar": 1 + }, + "valid": true + }, + { + "description": "object with strings is invalid", + "data": { + "foo\nbar": "1", + "foo\"bar": "1", + "foo\\bar": "1", + "foo\rbar": "1", + "foo\tbar": "1", + "foo\fbar": "1" + }, + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/propertyNames.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/propertyNames.json new file mode 100755 index 000000000000..f0788e649ff0 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/propertyNames.json @@ -0,0 +1,107 @@ +[ + { + "description": "propertyNames validation", + "schema": { + "propertyNames": {"maxLength": 3} + }, + "tests": [ + { + "description": "all property names valid", + "data": { + "f": {}, + "foo": {} + }, + "valid": true + }, + { + "description": "some property names invalid", + "data": { + "foo": {}, + "foobar": {} + }, + "valid": false + }, + { + "description": "object without properties is valid", + "data": {}, + "valid": true + }, + { + "description": "ignores arrays", + "data": [1, 2, 3, 4], + "valid": true + }, + { + "description": "ignores strings", + "data": "foobar", + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + } + ] + }, + { + "description": "propertyNames validation with pattern", + "schema": { + "propertyNames": { "pattern": "^a+$" } + }, + "tests": [ + { + "description": "matching property names valid", + "data": { + "a": {}, + "aa": {}, + "aaa": {} + }, + "valid": true + }, + { + "description": "non-matching property name is invalid", + "data": { + "aaA": {} + }, + "valid": false + }, + { + "description": "object without properties is valid", + "data": {}, + "valid": true + } + ] + }, + { + "description": "propertyNames with boolean schema true", + "schema": {"propertyNames": true}, + "tests": [ + { + "description": "object with any properties is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + } + ] + }, + { + "description": "propertyNames with boolean schema false", + "schema": {"propertyNames": false}, + "tests": [ + { + "description": "object with any properties is invalid", + "data": {"foo": 1}, + "valid": false + }, + { + "description": "empty object is valid", + "data": {}, + "valid": true + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/ref.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/ref.json new file mode 100755 index 000000000000..ce5caf674d45 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/ref.json @@ -0,0 +1,612 @@ +[ + { + "description": "root pointer ref", + "schema": { + "properties": { + "foo": {"$ref": "#"} + }, + "additionalProperties": false + }, + "tests": [ + { + "description": "match", + "data": {"foo": false}, + "valid": true + }, + { + "description": "recursive match", + "data": {"foo": {"foo": false}}, + "valid": true + }, + { + "description": "mismatch", + "data": {"bar": false}, + "valid": false + }, + { + "description": "recursive mismatch", + "data": {"foo": {"bar": false}}, + "valid": false + } + ] + }, + { + "description": "relative pointer ref to object", + "schema": { + "properties": { + "foo": {"type": "integer"}, + "bar": {"$ref": "#/properties/foo"} + } + }, + "tests": [ + { + "description": "match", + "data": {"bar": 3}, + "valid": true + }, + { + "description": "mismatch", + "data": {"bar": true}, + "valid": false + } + ] + }, + { + "description": "relative pointer ref to array", + "schema": { + "items": [ + {"type": "integer"}, + {"$ref": "#/items/0"} + ] + }, + "tests": [ + { + "description": "match array", + "data": [1, 2], + "valid": true + }, + { + "description": "mismatch array", + "data": [1, "foo"], + "valid": false + } + ] + }, + { + "description": "escaped pointer ref", + "schema": { + "definitions": { + "tilde~field": {"type": "integer"}, + "slash/field": {"type": "integer"}, + "percent%field": {"type": "integer"} + }, + "properties": { + "tilde": {"$ref": "#/definitions/tilde~0field"}, + "slash": {"$ref": "#/definitions/slash~1field"}, + "percent": {"$ref": "#/definitions/percent%25field"} + } + }, + "tests": [ + { + "description": "slash invalid", + "data": {"slash": "aoeu"}, + "valid": false + }, + { + "description": "tilde invalid", + "data": {"tilde": "aoeu"}, + "valid": false + }, + { + "description": "percent invalid", + "data": {"percent": "aoeu"}, + "valid": false + }, + { + "description": "slash valid", + "data": {"slash": 123}, + "valid": true + }, + { + "description": "tilde valid", + "data": {"tilde": 123}, + "valid": true + }, + { + "description": "percent valid", + "data": {"percent": 123}, + "valid": true + } + ] + }, + { + "description": "nested refs", + "schema": { + "definitions": { + "a": {"type": "integer"}, + "b": {"$ref": "#/definitions/a"}, + "c": {"$ref": "#/definitions/b"} + }, + "allOf": [{ "$ref": "#/definitions/c" }] + }, + "tests": [ + { + "description": "nested ref valid", + "data": 5, + "valid": true + }, + { + "description": "nested ref invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "ref overrides any sibling keywords", + "schema": { + "definitions": { + "reffed": { + "type": "array" + } + }, + "properties": { + "foo": { + "$ref": "#/definitions/reffed", + "maxItems": 2 + } + } + }, + "tests": [ + { + "description": "ref valid", + "data": { "foo": [] }, + "valid": true + }, + { + "description": "ref valid, maxItems ignored", + "data": { "foo": [ 1, 2, 3] }, + "valid": true + }, + { + "description": "ref invalid", + "data": { "foo": "string" }, + "valid": false + } + ] + }, + { + "description": "$ref prevents a sibling $id from changing the base uri", + "schema": { + "$id": "http://localhost:1234/sibling_id/base/", + "definitions": { + "foo": { + "$id": "http://localhost:1234/sibling_id/foo.json", + "type": "string" + }, + "base_foo": { + "$comment": "this canonical uri is http://localhost:1234/sibling_id/base/foo.json", + "$id": "foo.json", + "type": "number" + } + }, + "allOf": [ + { + "$comment": "$ref resolves to http://localhost:1234/sibling_id/base/foo.json, not http://localhost:1234/sibling_id/foo.json", + "$id": "http://localhost:1234/sibling_id/", + "$ref": "foo.json" + } + ] + }, + "tests": [ + { + "description": "$ref resolves to /definitions/base_foo, data does not validate", + "data": "a", + "valid": false + }, + { + "description": "$ref resolves to /definitions/base_foo, data validates", + "data": 1, + "valid": true + } + ] + }, + { + "description": "remote ref, containing refs itself", + "schema": {"$ref": "http://json-schema.org/draft-06/schema#"}, + "tests": [ + { + "description": "remote ref valid", + "data": {"minLength": 1}, + "valid": true + }, + { + "description": "remote ref invalid", + "data": {"minLength": -1}, + "valid": false + } + ] + }, + { + "description": "property named $ref that is not a reference", + "schema": { + "properties": { + "$ref": {"type": "string"} + } + }, + "tests": [ + { + "description": "property named $ref valid", + "data": {"$ref": "a"}, + "valid": true + }, + { + "description": "property named $ref invalid", + "data": {"$ref": 2}, + "valid": false + } + ] + }, + { + "description": "property named $ref, containing an actual $ref", + "schema": { + "properties": { + "$ref": {"$ref": "#/definitions/is-string"} + }, + "definitions": { + "is-string": { + "type": "string" + } + } + }, + "tests": [ + { + "description": "property named $ref valid", + "data": {"$ref": "a"}, + "valid": true + }, + { + "description": "property named $ref invalid", + "data": {"$ref": 2}, + "valid": false + } + ] + }, + { + "description": "$ref to boolean schema true", + "schema": { + "allOf": [{ "$ref": "#/definitions/bool" }], + "definitions": { + "bool": true + } + }, + "tests": [ + { + "description": "any value is valid", + "data": "foo", + "valid": true + } + ] + }, + { + "description": "$ref to boolean schema false", + "schema": { + "allOf": [{ "$ref": "#/definitions/bool" }], + "definitions": { + "bool": false + } + }, + "tests": [ + { + "description": "any value is invalid", + "data": "foo", + "valid": false + } + ] + }, + { + "description": "Recursive references between schemas", + "schema": { + "$id": "http://localhost:1234/tree", + "description": "tree of nodes", + "type": "object", + "properties": { + "meta": {"type": "string"}, + "nodes": { + "type": "array", + "items": {"$ref": "node"} + } + }, + "required": ["meta", "nodes"], + "definitions": { + "node": { + "$id": "http://localhost:1234/node", + "description": "node", + "type": "object", + "properties": { + "value": {"type": "number"}, + "subtree": {"$ref": "tree"} + }, + "required": ["value"] + } + } + }, + "tests": [ + { + "description": "valid tree", + "data": { + "meta": "root", + "nodes": [ + { + "value": 1, + "subtree": { + "meta": "child", + "nodes": [ + {"value": 1.1}, + {"value": 1.2} + ] + } + }, + { + "value": 2, + "subtree": { + "meta": "child", + "nodes": [ + {"value": 2.1}, + {"value": 2.2} + ] + } + } + ] + }, + "valid": true + }, + { + "description": "invalid tree", + "data": { + "meta": "root", + "nodes": [ + { + "value": 1, + "subtree": { + "meta": "child", + "nodes": [ + {"value": "string is invalid"}, + {"value": 1.2} + ] + } + }, + { + "value": 2, + "subtree": { + "meta": "child", + "nodes": [ + {"value": 2.1}, + {"value": 2.2} + ] + } + } + ] + }, + "valid": false + } + ] + }, + { + "description": "refs with quote", + "schema": { + "properties": { + "foo\"bar": {"$ref": "#/definitions/foo%22bar"} + }, + "definitions": { + "foo\"bar": {"type": "number"} + } + }, + "tests": [ + { + "description": "object with numbers is valid", + "data": { + "foo\"bar": 1 + }, + "valid": true + }, + { + "description": "object with strings is invalid", + "data": { + "foo\"bar": "1" + }, + "valid": false + } + ] + }, + { + "description": "Location-independent identifier", + "schema": { + "allOf": [{ + "$ref": "#foo" + }], + "definitions": { + "A": { + "$id": "#foo", + "type": "integer" + } + } + }, + "tests": [ + { + "data": 1, + "description": "match", + "valid": true + }, + { + "data": "a", + "description": "mismatch", + "valid": false + } + ] + }, + { + "description": "Location-independent identifier with base URI change in subschema", + "schema": { + "$id": "http://localhost:1234/root", + "allOf": [{ + "$ref": "http://localhost:1234/nested.json#foo" + }], + "definitions": { + "A": { + "$id": "nested.json", + "definitions": { + "B": { + "$id": "#foo", + "type": "integer" + } + } + } + } + }, + "tests": [ + { + "data": 1, + "description": "match", + "valid": true + }, + { + "data": "a", + "description": "mismatch", + "valid": false + } + ] + }, + { + "description": "naive replacement of $ref with its destination is not correct", + "schema": { + "definitions": { + "a_string": { "type": "string" } + }, + "enum": [ + { "$ref": "#/definitions/a_string" } + ] + }, + "tests": [ + { + "description": "do not evaluate the $ref inside the enum, matching any string", + "data": "this is a string", + "valid": false + }, + { + "description": "do not evaluate the $ref inside the enum, definition exact match", + "data": { "type": "string" }, + "valid": false + }, + { + "description": "match the enum exactly", + "data": { "$ref": "#/definitions/a_string" }, + "valid": true + } + ] + }, + { + "description": "refs with relative uris and defs", + "schema": { + "$id": "http://example.com/schema-relative-uri-defs1.json", + "properties": { + "foo": { + "$id": "schema-relative-uri-defs2.json", + "definitions": { + "inner": { + "properties": { + "bar": { "type": "string" } + } + } + }, + "allOf": [ { "$ref": "#/definitions/inner" } ] + } + }, + "allOf": [ { "$ref": "schema-relative-uri-defs2.json" } ] + }, + "tests": [ + { + "description": "invalid on inner field", + "data": { + "foo": { + "bar": 1 + }, + "bar": "a" + }, + "valid": false + }, + { + "description": "invalid on outer field", + "data": { + "foo": { + "bar": "a" + }, + "bar": 1 + }, + "valid": false + }, + { + "description": "valid on both fields", + "data": { + "foo": { + "bar": "a" + }, + "bar": "a" + }, + "valid": true + } + ] + }, + { + "description": "relative refs with absolute uris and defs", + "schema": { + "$id": "http://example.com/schema-refs-absolute-uris-defs1.json", + "properties": { + "foo": { + "$id": "http://example.com/schema-refs-absolute-uris-defs2.json", + "definitions": { + "inner": { + "properties": { + "bar": { "type": "string" } + } + } + }, + "allOf": [ { "$ref": "#/definitions/inner" } ] + } + }, + "allOf": [ { "$ref": "schema-refs-absolute-uris-defs2.json" } ] + }, + "tests": [ + { + "description": "invalid on inner field", + "data": { + "foo": { + "bar": 1 + }, + "bar": "a" + }, + "valid": false + }, + { + "description": "invalid on outer field", + "data": { + "foo": { + "bar": "a" + }, + "bar": 1 + }, + "valid": false + }, + { + "description": "valid on both fields", + "data": { + "foo": { + "bar": "a" + }, + "bar": "a" + }, + "valid": true + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/refRemote.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/refRemote.json new file mode 100755 index 000000000000..a2221b21390a --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/refRemote.json @@ -0,0 +1,196 @@ +[ + { + "description": "remote ref", + "schema": {"$ref": "http://localhost:1234/integer.json"}, + "tests": [ + { + "description": "remote ref valid", + "data": 1, + "valid": true + }, + { + "description": "remote ref invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "fragment within remote ref", + "schema": {"$ref": "http://localhost:1234/subSchemas.json#/integer"}, + "tests": [ + { + "description": "remote fragment valid", + "data": 1, + "valid": true + }, + { + "description": "remote fragment invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "ref within remote ref", + "schema": { + "$ref": "http://localhost:1234/subSchemas.json#/refToInteger" + }, + "tests": [ + { + "description": "ref within ref valid", + "data": 1, + "valid": true + }, + { + "description": "ref within ref invalid", + "data": "a", + "valid": false + } + ] + }, + { + "description": "base URI change", + "schema": { + "$id": "http://localhost:1234/", + "items": { + "$id": "baseUriChange/", + "items": {"$ref": "folderInteger.json"} + } + }, + "tests": [ + { + "description": "base URI change ref valid", + "data": [[1]], + "valid": true + }, + { + "description": "base URI change ref invalid", + "data": [["a"]], + "valid": false + } + ] + }, + { + "description": "base URI change - change folder", + "schema": { + "$id": "http://localhost:1234/scope_change_defs1.json", + "type" : "object", + "properties": { + "list": {"$ref": "#/definitions/baz"} + }, + "definitions": { + "baz": { + "$id": "baseUriChangeFolder/", + "type": "array", + "items": {"$ref": "folderInteger.json"} + } + } + }, + "tests": [ + { + "description": "number is valid", + "data": {"list": [1]}, + "valid": true + }, + { + "description": "string is invalid", + "data": {"list": ["a"]}, + "valid": false + } + ] + }, + { + "description": "base URI change - change folder in subschema", + "schema": { + "$id": "http://localhost:1234/scope_change_defs2.json", + "type" : "object", + "properties": { + "list": {"$ref": "#/definitions/baz/definitions/bar"} + }, + "definitions": { + "baz": { + "$id": "baseUriChangeFolderInSubschema/", + "definitions": { + "bar": { + "type": "array", + "items": {"$ref": "folderInteger.json"} + } + } + } + } + }, + "tests": [ + { + "description": "number is valid", + "data": {"list": [1]}, + "valid": true + }, + { + "description": "string is invalid", + "data": {"list": ["a"]}, + "valid": false + } + ] + }, + { + "description": "root ref in remote ref", + "schema": { + "$id": "http://localhost:1234/object", + "type": "object", + "properties": { + "name": {"$ref": "name.json#/definitions/orNull"} + } + }, + "tests": [ + { + "description": "string is valid", + "data": { + "name": "foo" + }, + "valid": true + }, + { + "description": "null is valid", + "data": { + "name": null + }, + "valid": true + }, + { + "description": "object is invalid", + "data": { + "name": { + "name": null + } + }, + "valid": false + } + ] + }, + { + "description": "remote ref with ref to definitions", + "schema": { + "$id": "http://localhost:1234/schema-remote-ref-ref-defs1.json", + "allOf": [ + { "$ref": "ref-and-definitions.json" } + ] + }, + "tests": [ + { + "description": "invalid", + "data": { + "bar": 1 + }, + "valid": false + }, + { + "description": "valid", + "data": { + "bar": "a" + }, + "valid": true + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/required.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/required.json new file mode 100755 index 000000000000..abf18f345941 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/required.json @@ -0,0 +1,105 @@ +[ + { + "description": "required validation", + "schema": { + "properties": { + "foo": {}, + "bar": {} + }, + "required": ["foo"] + }, + "tests": [ + { + "description": "present required property is valid", + "data": {"foo": 1}, + "valid": true + }, + { + "description": "non-present required property is invalid", + "data": {"bar": 1}, + "valid": false + }, + { + "description": "ignores arrays", + "data": [], + "valid": true + }, + { + "description": "ignores strings", + "data": "", + "valid": true + }, + { + "description": "ignores other non-objects", + "data": 12, + "valid": true + } + ] + }, + { + "description": "required default validation", + "schema": { + "properties": { + "foo": {} + } + }, + "tests": [ + { + "description": "not required by default", + "data": {}, + "valid": true + } + ] + }, + { + "description": "required with empty array", + "schema": { + "properties": { + "foo": {} + }, + "required": [] + }, + "tests": [ + { + "description": "property not required", + "data": {}, + "valid": true + } + ] + }, + { + "description": "required with escaped characters", + "schema": { + "required": [ + "foo\nbar", + "foo\"bar", + "foo\\bar", + "foo\rbar", + "foo\tbar", + "foo\fbar" + ] + }, + "tests": [ + { + "description": "object with all properties present is valid", + "data": { + "foo\nbar": 1, + "foo\"bar": 1, + "foo\\bar": 1, + "foo\rbar": 1, + "foo\tbar": 1, + "foo\fbar": 1 + }, + "valid": true + }, + { + "description": "object with some properties missing is invalid", + "data": { + "foo\nbar": "1", + "foo\"bar": "1" + }, + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/type.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/type.json new file mode 100755 index 000000000000..830464702982 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/type.json @@ -0,0 +1,474 @@ +[ + { + "description": "integer type matches integers", + "schema": {"type": "integer"}, + "tests": [ + { + "description": "an integer is an integer", + "data": 1, + "valid": true + }, + { + "description": "a float with zero fractional part is an integer", + "data": 1.0, + "valid": true + }, + { + "description": "a float is not an integer", + "data": 1.1, + "valid": false + }, + { + "description": "a string is not an integer", + "data": "foo", + "valid": false + }, + { + "description": "a string is still not an integer, even if it looks like one", + "data": "1", + "valid": false + }, + { + "description": "an object is not an integer", + "data": {}, + "valid": false + }, + { + "description": "an array is not an integer", + "data": [], + "valid": false + }, + { + "description": "a boolean is not an integer", + "data": true, + "valid": false + }, + { + "description": "null is not an integer", + "data": null, + "valid": false + } + ] + }, + { + "description": "number type matches numbers", + "schema": {"type": "number"}, + "tests": [ + { + "description": "an integer is a number", + "data": 1, + "valid": true + }, + { + "description": "a float with zero fractional part is a number (and an integer)", + "data": 1.0, + "valid": true + }, + { + "description": "a float is a number", + "data": 1.1, + "valid": true + }, + { + "description": "a string is not a number", + "data": "foo", + "valid": false + }, + { + "description": "a string is still not a number, even if it looks like one", + "data": "1", + "valid": false + }, + { + "description": "an object is not a number", + "data": {}, + "valid": false + }, + { + "description": "an array is not a number", + "data": [], + "valid": false + }, + { + "description": "a boolean is not a number", + "data": true, + "valid": false + }, + { + "description": "null is not a number", + "data": null, + "valid": false + } + ] + }, + { + "description": "string type matches strings", + "schema": {"type": "string"}, + "tests": [ + { + "description": "1 is not a string", + "data": 1, + "valid": false + }, + { + "description": "a float is not a string", + "data": 1.1, + "valid": false + }, + { + "description": "a string is a string", + "data": "foo", + "valid": true + }, + { + "description": "a string is still a string, even if it looks like a number", + "data": "1", + "valid": true + }, + { + "description": "an empty string is still a string", + "data": "", + "valid": true + }, + { + "description": "an object is not a string", + "data": {}, + "valid": false + }, + { + "description": "an array is not a string", + "data": [], + "valid": false + }, + { + "description": "a boolean is not a string", + "data": true, + "valid": false + }, + { + "description": "null is not a string", + "data": null, + "valid": false + } + ] + }, + { + "description": "object type matches objects", + "schema": {"type": "object"}, + "tests": [ + { + "description": "an integer is not an object", + "data": 1, + "valid": false + }, + { + "description": "a float is not an object", + "data": 1.1, + "valid": false + }, + { + "description": "a string is not an object", + "data": "foo", + "valid": false + }, + { + "description": "an object is an object", + "data": {}, + "valid": true + }, + { + "description": "an array is not an object", + "data": [], + "valid": false + }, + { + "description": "a boolean is not an object", + "data": true, + "valid": false + }, + { + "description": "null is not an object", + "data": null, + "valid": false + } + ] + }, + { + "description": "array type matches arrays", + "schema": {"type": "array"}, + "tests": [ + { + "description": "an integer is not an array", + "data": 1, + "valid": false + }, + { + "description": "a float is not an array", + "data": 1.1, + "valid": false + }, + { + "description": "a string is not an array", + "data": "foo", + "valid": false + }, + { + "description": "an object is not an array", + "data": {}, + "valid": false + }, + { + "description": "an array is an array", + "data": [], + "valid": true + }, + { + "description": "a boolean is not an array", + "data": true, + "valid": false + }, + { + "description": "null is not an array", + "data": null, + "valid": false + } + ] + }, + { + "description": "boolean type matches booleans", + "schema": {"type": "boolean"}, + "tests": [ + { + "description": "an integer is not a boolean", + "data": 1, + "valid": false + }, + { + "description": "zero is not a boolean", + "data": 0, + "valid": false + }, + { + "description": "a float is not a boolean", + "data": 1.1, + "valid": false + }, + { + "description": "a string is not a boolean", + "data": "foo", + "valid": false + }, + { + "description": "an empty string is not a boolean", + "data": "", + "valid": false + }, + { + "description": "an object is not a boolean", + "data": {}, + "valid": false + }, + { + "description": "an array is not a boolean", + "data": [], + "valid": false + }, + { + "description": "true is a boolean", + "data": true, + "valid": true + }, + { + "description": "false is a boolean", + "data": false, + "valid": true + }, + { + "description": "null is not a boolean", + "data": null, + "valid": false + } + ] + }, + { + "description": "null type matches only the null object", + "schema": {"type": "null"}, + "tests": [ + { + "description": "an integer is not null", + "data": 1, + "valid": false + }, + { + "description": "a float is not null", + "data": 1.1, + "valid": false + }, + { + "description": "zero is not null", + "data": 0, + "valid": false + }, + { + "description": "a string is not null", + "data": "foo", + "valid": false + }, + { + "description": "an empty string is not null", + "data": "", + "valid": false + }, + { + "description": "an object is not null", + "data": {}, + "valid": false + }, + { + "description": "an array is not null", + "data": [], + "valid": false + }, + { + "description": "true is not null", + "data": true, + "valid": false + }, + { + "description": "false is not null", + "data": false, + "valid": false + }, + { + "description": "null is null", + "data": null, + "valid": true + } + ] + }, + { + "description": "multiple types can be specified in an array", + "schema": {"type": ["integer", "string"]}, + "tests": [ + { + "description": "an integer is valid", + "data": 1, + "valid": true + }, + { + "description": "a string is valid", + "data": "foo", + "valid": true + }, + { + "description": "a float is invalid", + "data": 1.1, + "valid": false + }, + { + "description": "an object is invalid", + "data": {}, + "valid": false + }, + { + "description": "an array is invalid", + "data": [], + "valid": false + }, + { + "description": "a boolean is invalid", + "data": true, + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + } + ] + }, + { + "description": "type as array with one item", + "schema": { + "type": ["string"] + }, + "tests": [ + { + "description": "string is valid", + "data": "foo", + "valid": true + }, + { + "description": "number is invalid", + "data": 123, + "valid": false + } + ] + }, + { + "description": "type: array or object", + "schema": { + "type": ["array", "object"] + }, + "tests": [ + { + "description": "array is valid", + "data": [1,2,3], + "valid": true + }, + { + "description": "object is valid", + "data": {"foo": 123}, + "valid": true + }, + { + "description": "number is invalid", + "data": 123, + "valid": false + }, + { + "description": "string is invalid", + "data": "foo", + "valid": false + }, + { + "description": "null is invalid", + "data": null, + "valid": false + } + ] + }, + { + "description": "type: array, object or null", + "schema": { + "type": ["array", "object", "null"] + }, + "tests": [ + { + "description": "array is valid", + "data": [1,2,3], + "valid": true + }, + { + "description": "object is valid", + "data": {"foo": 123}, + "valid": true + }, + { + "description": "null is valid", + "data": null, + "valid": true + }, + { + "description": "number is invalid", + "data": 123, + "valid": false + }, + { + "description": "string is invalid", + "data": "foo", + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/uniqueItems.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/uniqueItems.json new file mode 100755 index 000000000000..2ccf666d7dfd --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/uniqueItems.json @@ -0,0 +1,404 @@ +[ + { + "description": "uniqueItems validation", + "schema": {"uniqueItems": true}, + "tests": [ + { + "description": "unique array of integers is valid", + "data": [1, 2], + "valid": true + }, + { + "description": "non-unique array of integers is invalid", + "data": [1, 1], + "valid": false + }, + { + "description": "non-unique array of more than two integers is invalid", + "data": [1, 2, 1], + "valid": false + }, + { + "description": "numbers are unique if mathematically unequal", + "data": [1.0, 1.00, 1], + "valid": false + }, + { + "description": "false is not equal to zero", + "data": [0, false], + "valid": true + }, + { + "description": "true is not equal to one", + "data": [1, true], + "valid": true + }, + { + "description": "unique array of strings is valid", + "data": ["foo", "bar", "baz"], + "valid": true + }, + { + "description": "non-unique array of strings is invalid", + "data": ["foo", "bar", "foo"], + "valid": false + }, + { + "description": "unique array of objects is valid", + "data": [{"foo": "bar"}, {"foo": "baz"}], + "valid": true + }, + { + "description": "non-unique array of objects is invalid", + "data": [{"foo": "bar"}, {"foo": "bar"}], + "valid": false + }, + { + "description": "unique array of nested objects is valid", + "data": [ + {"foo": {"bar" : {"baz" : true}}}, + {"foo": {"bar" : {"baz" : false}}} + ], + "valid": true + }, + { + "description": "non-unique array of nested objects is invalid", + "data": [ + {"foo": {"bar" : {"baz" : true}}}, + {"foo": {"bar" : {"baz" : true}}} + ], + "valid": false + }, + { + "description": "unique array of arrays is valid", + "data": [["foo"], ["bar"]], + "valid": true + }, + { + "description": "non-unique array of arrays is invalid", + "data": [["foo"], ["foo"]], + "valid": false + }, + { + "description": "non-unique array of more than two arrays is invalid", + "data": [["foo"], ["bar"], ["foo"]], + "valid": false + }, + { + "description": "1 and true are unique", + "data": [1, true], + "valid": true + }, + { + "description": "0 and false are unique", + "data": [0, false], + "valid": true + }, + { + "description": "[1] and [true] are unique", + "data": [[1], [true]], + "valid": true + }, + { + "description": "[0] and [false] are unique", + "data": [[0], [false]], + "valid": true + }, + { + "description": "nested [1] and [true] are unique", + "data": [[[1], "foo"], [[true], "foo"]], + "valid": true + }, + { + "description": "nested [0] and [false] are unique", + "data": [[[0], "foo"], [[false], "foo"]], + "valid": true + }, + { + "description": "unique heterogeneous types are valid", + "data": [{}, [1], true, null, 1, "{}"], + "valid": true + }, + { + "description": "non-unique heterogeneous types are invalid", + "data": [{}, [1], true, null, {}, 1], + "valid": false + }, + { + "description": "different objects are unique", + "data": [{"a": 1, "b": 2}, {"a": 2, "b": 1}], + "valid": true + }, + { + "description": "objects are non-unique despite key order", + "data": [{"a": 1, "b": 2}, {"b": 2, "a": 1}], + "valid": false + }, + { + "description": "{\"a\": false} and {\"a\": 0} are unique", + "data": [{"a": false}, {"a": 0}], + "valid": true + }, + { + "description": "{\"a\": true} and {\"a\": 1} are unique", + "data": [{"a": true}, {"a": 1}], + "valid": true + } + ] + }, + { + "description": "uniqueItems with an array of items", + "schema": { + "items": [{"type": "boolean"}, {"type": "boolean"}], + "uniqueItems": true + }, + "tests": [ + { + "description": "[false, true] from items array is valid", + "data": [false, true], + "valid": true + }, + { + "description": "[true, false] from items array is valid", + "data": [true, false], + "valid": true + }, + { + "description": "[false, false] from items array is not valid", + "data": [false, false], + "valid": false + }, + { + "description": "[true, true] from items array is not valid", + "data": [true, true], + "valid": false + }, + { + "description": "unique array extended from [false, true] is valid", + "data": [false, true, "foo", "bar"], + "valid": true + }, + { + "description": "unique array extended from [true, false] is valid", + "data": [true, false, "foo", "bar"], + "valid": true + }, + { + "description": "non-unique array extended from [false, true] is not valid", + "data": [false, true, "foo", "foo"], + "valid": false + }, + { + "description": "non-unique array extended from [true, false] is not valid", + "data": [true, false, "foo", "foo"], + "valid": false + } + ] + }, + { + "description": "uniqueItems with an array of items and additionalItems=false", + "schema": { + "items": [{"type": "boolean"}, {"type": "boolean"}], + "uniqueItems": true, + "additionalItems": false + }, + "tests": [ + { + "description": "[false, true] from items array is valid", + "data": [false, true], + "valid": true + }, + { + "description": "[true, false] from items array is valid", + "data": [true, false], + "valid": true + }, + { + "description": "[false, false] from items array is not valid", + "data": [false, false], + "valid": false + }, + { + "description": "[true, true] from items array is not valid", + "data": [true, true], + "valid": false + }, + { + "description": "extra items are invalid even if unique", + "data": [false, true, null], + "valid": false + } + ] + }, + { + "description": "uniqueItems=false validation", + "schema": { "uniqueItems": false }, + "tests": [ + { + "description": "unique array of integers is valid", + "data": [1, 2], + "valid": true + }, + { + "description": "non-unique array of integers is valid", + "data": [1, 1], + "valid": true + }, + { + "description": "numbers are unique if mathematically unequal", + "data": [1.0, 1.00, 1], + "valid": true + }, + { + "description": "false is not equal to zero", + "data": [0, false], + "valid": true + }, + { + "description": "true is not equal to one", + "data": [1, true], + "valid": true + }, + { + "description": "unique array of objects is valid", + "data": [{"foo": "bar"}, {"foo": "baz"}], + "valid": true + }, + { + "description": "non-unique array of objects is valid", + "data": [{"foo": "bar"}, {"foo": "bar"}], + "valid": true + }, + { + "description": "unique array of nested objects is valid", + "data": [ + {"foo": {"bar" : {"baz" : true}}}, + {"foo": {"bar" : {"baz" : false}}} + ], + "valid": true + }, + { + "description": "non-unique array of nested objects is valid", + "data": [ + {"foo": {"bar" : {"baz" : true}}}, + {"foo": {"bar" : {"baz" : true}}} + ], + "valid": true + }, + { + "description": "unique array of arrays is valid", + "data": [["foo"], ["bar"]], + "valid": true + }, + { + "description": "non-unique array of arrays is valid", + "data": [["foo"], ["foo"]], + "valid": true + }, + { + "description": "1 and true are unique", + "data": [1, true], + "valid": true + }, + { + "description": "0 and false are unique", + "data": [0, false], + "valid": true + }, + { + "description": "unique heterogeneous types are valid", + "data": [{}, [1], true, null, 1], + "valid": true + }, + { + "description": "non-unique heterogeneous types are valid", + "data": [{}, [1], true, null, {}, 1], + "valid": true + } + ] + }, + { + "description": "uniqueItems=false with an array of items", + "schema": { + "items": [{"type": "boolean"}, {"type": "boolean"}], + "uniqueItems": false + }, + "tests": [ + { + "description": "[false, true] from items array is valid", + "data": [false, true], + "valid": true + }, + { + "description": "[true, false] from items array is valid", + "data": [true, false], + "valid": true + }, + { + "description": "[false, false] from items array is valid", + "data": [false, false], + "valid": true + }, + { + "description": "[true, true] from items array is valid", + "data": [true, true], + "valid": true + }, + { + "description": "unique array extended from [false, true] is valid", + "data": [false, true, "foo", "bar"], + "valid": true + }, + { + "description": "unique array extended from [true, false] is valid", + "data": [true, false, "foo", "bar"], + "valid": true + }, + { + "description": "non-unique array extended from [false, true] is valid", + "data": [false, true, "foo", "foo"], + "valid": true + }, + { + "description": "non-unique array extended from [true, false] is valid", + "data": [true, false, "foo", "foo"], + "valid": true + } + ] + }, + { + "description": "uniqueItems=false with an array of items and additionalItems=false", + "schema": { + "items": [{"type": "boolean"}, {"type": "boolean"}], + "uniqueItems": false, + "additionalItems": false + }, + "tests": [ + { + "description": "[false, true] from items array is valid", + "data": [false, true], + "valid": true + }, + { + "description": "[true, false] from items array is valid", + "data": [true, false], + "valid": true + }, + { + "description": "[false, false] from items array is valid", + "data": [false, false], + "valid": true + }, + { + "description": "[true, true] from items array is valid", + "data": [true, true], + "valid": true + }, + { + "description": "extra items are invalid even if unique", + "data": [false, true, null], + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/unknownKeyword.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/unknownKeyword.json new file mode 100755 index 000000000000..1f58d97e3e66 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/draft6/unknownKeyword.json @@ -0,0 +1,56 @@ +[ + { + "description": "$id inside an unknown keyword is not a real identifier", + "comment": "the implementation must not be confused by an $id in locations we do not know how to parse", + "schema": { + "definitions": { + "id_in_unknown0": { + "not": { + "array_of_schemas": [ + { + "$id": "https://localhost:1234/unknownKeyword/my_identifier.json", + "type": "null" + } + ] + } + }, + "real_id_in_schema": { + "$id": "https://localhost:1234/unknownKeyword/my_identifier.json", + "type": "string" + }, + "id_in_unknown1": { + "not": { + "object_of_schemas": { + "foo": { + "$id": "https://localhost:1234/unknownKeyword/my_identifier.json", + "type": "integer" + } + } + } + } + }, + "anyOf": [ + { "$ref": "#/definitions/id_in_unknown0" }, + { "$ref": "#/definitions/id_in_unknown1" }, + { "$ref": "https://localhost:1234/unknownKeyword/my_identifier.json" } + ] + }, + "tests": [ + { + "description": "type matches second anyOf, which has a real schema in it", + "data": "a string", + "valid": true + }, + { + "description": "type matches non-schema in first anyOf", + "data": null, + "valid": false + }, + { + "description": "type matches non-schema in third anyOf", + "data": 1, + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/openapi_additions/type.json b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/openapi_additions/type.json new file mode 100755 index 000000000000..2125a444303a --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/openapi_additions/type.json @@ -0,0 +1,43 @@ +[ + { + "description": "array type matches arrays", + "schema": {"type": "array", "items": {}}, + "tests": [ + { + "description": "an integer is not an array", + "data": 1, + "valid": false + }, + { + "description": "a float is not an array", + "data": 1.1, + "valid": false + }, + { + "description": "a string is not an array", + "data": "foo", + "valid": false + }, + { + "description": "an object is not an array", + "data": {}, + "valid": false + }, + { + "description": "an array is an array", + "data": [], + "valid": true + }, + { + "description": "a boolean is not an array", + "data": true, + "valid": false + }, + { + "description": "null is not an array", + "data": null, + "valid": false + } + ] + } +] diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/spec_writer.py b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/spec_writer.py new file mode 100644 index 000000000000..854c3694f563 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/spec_writer.py @@ -0,0 +1,189 @@ +import dataclasses +import re +import json +import pathlib +import typing + +import yaml + +""" +Swagger-parser can create one spec from multiple files BUT: +- it must use refs to values in another file +- those refs will be turned into component schemas $refs +- so it will not dereference and inline schemas + +Note: +- 3.0.3: schema supports only a single example +- That example could $ref a component example but we have not ability to test negative examples +- Higher spec versions do have examples BUT: examples does not store pass fail info + - that info could be added as an x-flag + +components.examples Re-use solution: +- including an x-valid tag in each example to note if it should pass or fail +- v3.0.3: add x-examples to schema for passing and failing examples +- ref the needed examples +- using examples for passing examples + +BUT: +- the parser already supports refing examples to a separate yaml file any way + +TODO: +- [DONE] load json file and write it into components/schemas +- [DONE] ensure that examples are being written there +- [DONE] add recursive casting of test examples to ObjectWithTypeBooleans +- [DONE] move processing to defaultCodegen +- [DONE] turn tests on +- add endpoints with those components in: +- request body +- response body +- all parameter types +- add main spec that uses type spec +""" + +@dataclasses.dataclass +class JsonSchemaTestCase: + description: str + data: typing.Union[str, int, float, bool, None, list, dict] + valid: bool + +@dataclasses.dataclass +class JsonSchemaTestSchema: + description: str + schema: dict + tests: typing.List[JsonSchemaTestCase] + + +class ExclusionReason: + v303_does_not_support_array_of_types = 'v3.0.3 does not support type with array of values' + v303_requires_array_have_items = 'v3.0.3 requires that items MUST be present if the type is array' + +json_schema_test_draft = 'draft6' +filepath_to_excluded_case_and_reason = { + (json_schema_test_draft, 'type.json'): { + 'multiple types can be specified in an array': ExclusionReason.v303_does_not_support_array_of_types, + 'type as array with one item': ExclusionReason.v303_does_not_support_array_of_types, + 'type: array or object': ExclusionReason.v303_does_not_support_array_of_types, + 'type: array, object or null': ExclusionReason.v303_does_not_support_array_of_types, + 'array type matches arrays': ExclusionReason.v303_requires_array_have_items, + } +} + +def get_json_schema_test_schemas(file_path: typing.Tuple[str]) -> typing.List[JsonSchemaTestSchema]: + json_schema_test_schemas = [] + filename = file_path[-1] + excluded_case_to_reason = filepath_to_excluded_case_and_reason.get(file_path, {}) + path = pathlib.PurePath(*file_path) + with open(path) as json_file: + test_schema_dicts = json.load(json_file) + for test_schema_dict in test_schema_dicts: + test_schema_dict['tests'] = [JsonSchemaTestCase(**t) for t in test_schema_dict['tests']] + json_schema_test_schema = JsonSchemaTestSchema(**test_schema_dict) + test_case_desc = json_schema_test_schema.description + excluded_reason = excluded_case_to_reason.get(test_case_desc) + if excluded_reason: + print(f'Excluding {test_case_desc} because {excluded_reason}') + continue + + json_schema_test_schemas.append(json_schema_test_schema) + + return json_schema_test_schemas + +openapi_additions = 'openapi_additions' + +json_schema_test_file_to_folders = { + 'type.json': (json_schema_test_draft, openapi_additions) +} + +openapi_version = '3.0.3' + + +@dataclasses.dataclass +class OpenApiDocumentInfo: + title: str + description: str + version: str + +OpenApiSchema = typing.TypedDict( + 'OpenApiSchema', + { + 'type': str, + 'x-test-examples': typing.Dict[str, JsonSchemaTestCase], + 'items': 'OpenApiSchema' + } +) + +@dataclasses.dataclass +class OpenApiExample: + description: str + value: typing.Union[str, int, float, bool, None, list, dict] + +@dataclasses.dataclass +class OpenApiComponents: + schemas: typing.Dict[str, OpenApiSchema] + +@dataclasses.dataclass +class OpenApiDocument: + openapi: str + info: OpenApiDocumentInfo + paths: typing.Dict[str, typing.Any] + components: OpenApiComponents + + +def get_new_openapi() -> OpenApiDocument: + return OpenApiDocument( + openapi=openapi_version, + info=OpenApiDocumentInfo( + title=f"openapi {openapi_version} sample spec", + description=f"sample spec for testing openapi functionality, built from json schema tests for {json_schema_test_draft}", + version="0.0.1" + ), + paths={}, + components=OpenApiComponents( + schemas={}, + ) + ) + +def description_to_component_name(descr: str) -> str: + res = ''.join(descr.title().split()) + return re.sub(r'[^A-Za-z0-9 ]+', '', res) + +def get_test_case_name(test: JsonSchemaTestSchema) -> str: + res = ''.join(test.description.title().split()) + return re.sub(r'[^A-Za-z0-9 ]+', '', res) + +def get_openapi_from_test_file(json_schema_test_file: str, folders: typing.Tuple[str]) -> OpenApiDocument: + openapi = get_new_openapi() + for folder in folders: + file_path_tuple = (folder, json_schema_test_file) + test_schemas = get_json_schema_test_schemas(file_path_tuple) + for test_schema in test_schemas: + component_name = description_to_component_name(test_schema.description) + kwargs = { + 'x-test-examples': {} + } + schema = OpenApiSchema( + **test_schema.schema, **kwargs + ) + openapi.components.schemas[component_name] = schema + for test in test_schema.tests: + test_case_name = get_test_case_name(test) + schema['x-test-examples'][test_case_name] = test + return openapi + +for json_schema_test_file, folders in json_schema_test_file_to_folders.items(): + openapi = get_openapi_from_test_file(json_schema_test_file, folders) + print( + yaml.dump( + dataclasses.asdict(openapi), + sort_keys=False + ) + ) + filename_prefix = pathlib.Path(json_schema_test_file).stem + spec_out = f'{filename_prefix}.yaml' + with open(spec_out, 'w') as yaml_out: + yaml_out.write( + yaml.dump( + dataclasses.asdict(openapi), + sort_keys=False + ) + ) \ No newline at end of file diff --git a/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/type.yaml b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/type.yaml new file mode 100644 index 000000000000..d2991f6eaac1 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/unit_test_spec/type.yaml @@ -0,0 +1,275 @@ +openapi: 3.0.3 +info: + title: openapi 3.0.3 sample spec + description: sample spec for testing openapi functionality, built from json schema + tests for draft6 + version: 0.0.1 +paths: {} +components: + schemas: + IntegerTypeMatchesIntegers: + type: integer + x-test-examples: + AnIntegerIsAnInteger: + description: an integer is an integer + data: 1 + valid: true + AFloatWithZeroFractionalPartIsAnInteger: + description: a float with zero fractional part is an integer + data: 1.0 + valid: true + AFloatIsNotAnInteger: + description: a float is not an integer + data: 1.1 + valid: false + AStringIsNotAnInteger: + description: a string is not an integer + data: foo + valid: false + AStringIsStillNotAnIntegerEvenIfItLooksLikeOne: + description: a string is still not an integer, even if it looks like one + data: '1' + valid: false + AnObjectIsNotAnInteger: + description: an object is not an integer + data: {} + valid: false + AnArrayIsNotAnInteger: + description: an array is not an integer + data: [] + valid: false + ABooleanIsNotAnInteger: + description: a boolean is not an integer + data: true + valid: false + NullIsNotAnInteger: + description: null is not an integer + data: null + valid: false + NumberTypeMatchesNumbers: + type: number + x-test-examples: + AnIntegerIsANumber: + description: an integer is a number + data: 1 + valid: true + AFloatWithZeroFractionalPartIsANumberAndAnInteger: + description: a float with zero fractional part is a number (and an integer) + data: 1.0 + valid: true + AFloatIsANumber: + description: a float is a number + data: 1.1 + valid: true + AStringIsNotANumber: + description: a string is not a number + data: foo + valid: false + AStringIsStillNotANumberEvenIfItLooksLikeOne: + description: a string is still not a number, even if it looks like one + data: '1' + valid: false + AnObjectIsNotANumber: + description: an object is not a number + data: {} + valid: false + AnArrayIsNotANumber: + description: an array is not a number + data: [] + valid: false + ABooleanIsNotANumber: + description: a boolean is not a number + data: true + valid: false + NullIsNotANumber: + description: null is not a number + data: null + valid: false + StringTypeMatchesStrings: + type: string + x-test-examples: + 1IsNotAString: + description: 1 is not a string + data: 1 + valid: false + AFloatIsNotAString: + description: a float is not a string + data: 1.1 + valid: false + AStringIsAString: + description: a string is a string + data: foo + valid: true + AStringIsStillAStringEvenIfItLooksLikeANumber: + description: a string is still a string, even if it looks like a number + data: '1' + valid: true + AnEmptyStringIsStillAString: + description: an empty string is still a string + data: '' + valid: true + AnObjectIsNotAString: + description: an object is not a string + data: {} + valid: false + AnArrayIsNotAString: + description: an array is not a string + data: [] + valid: false + ABooleanIsNotAString: + description: a boolean is not a string + data: true + valid: false + NullIsNotAString: + description: null is not a string + data: null + valid: false + ObjectTypeMatchesObjects: + type: object + x-test-examples: + AnIntegerIsNotAnObject: + description: an integer is not an object + data: 1 + valid: false + AFloatIsNotAnObject: + description: a float is not an object + data: 1.1 + valid: false + AStringIsNotAnObject: + description: a string is not an object + data: foo + valid: false + AnObjectIsAnObject: + description: an object is an object + data: {} + valid: true + AnArrayIsNotAnObject: + description: an array is not an object + data: [] + valid: false + ABooleanIsNotAnObject: + description: a boolean is not an object + data: true + valid: false + NullIsNotAnObject: + description: null is not an object + data: null + valid: false + BooleanTypeMatchesBooleans: + type: boolean + x-test-examples: + AnIntegerIsNotABoolean: + description: an integer is not a boolean + data: 1 + valid: false + ZeroIsNotABoolean: + description: zero is not a boolean + data: 0 + valid: false + AFloatIsNotABoolean: + description: a float is not a boolean + data: 1.1 + valid: false + AStringIsNotABoolean: + description: a string is not a boolean + data: foo + valid: false + AnEmptyStringIsNotABoolean: + description: an empty string is not a boolean + data: '' + valid: false + AnObjectIsNotABoolean: + description: an object is not a boolean + data: {} + valid: false + AnArrayIsNotABoolean: + description: an array is not a boolean + data: [] + valid: false + TrueIsABoolean: + description: true is a boolean + data: true + valid: true + FalseIsABoolean: + description: false is a boolean + data: false + valid: true + NullIsNotABoolean: + description: null is not a boolean + data: null + valid: false + NullTypeMatchesOnlyTheNullObject: + type: 'null' + x-test-examples: + AnIntegerIsNotNull: + description: an integer is not null + data: 1 + valid: false + AFloatIsNotNull: + description: a float is not null + data: 1.1 + valid: false + ZeroIsNotNull: + description: zero is not null + data: 0 + valid: false + AStringIsNotNull: + description: a string is not null + data: foo + valid: false + AnEmptyStringIsNotNull: + description: an empty string is not null + data: '' + valid: false + AnObjectIsNotNull: + description: an object is not null + data: {} + valid: false + AnArrayIsNotNull: + description: an array is not null + data: [] + valid: false + TrueIsNotNull: + description: true is not null + data: true + valid: false + FalseIsNotNull: + description: false is not null + data: false + valid: false + NullIsNull: + description: null is null + data: null + valid: true + ArrayTypeMatchesArrays: + type: array + items: {} + x-test-examples: + AnIntegerIsNotAnArray: + description: an integer is not an array + data: 1 + valid: false + AFloatIsNotAnArray: + description: a float is not an array + data: 1.1 + valid: false + AStringIsNotAnArray: + description: a string is not an array + data: foo + valid: false + AnObjectIsNotAnArray: + description: an object is not an array + data: {} + valid: false + AnArrayIsAnArray: + description: an array is an array + data: [] + valid: true + ABooleanIsNotAnArray: + description: a boolean is not an array + data: true + valid: false + NullIsNotAnArray: + description: null is not an array + data: null + valid: false diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/.gitignore b/samples/openapi3/client/3_0_3_unit_test/python-experimental/.gitignore new file mode 100644 index 000000000000..a62e8aba43f8 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/.gitignore @@ -0,0 +1,67 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt +dev-requirements.txt.log + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ +venv/ +.venv/ +.python-version +.pytest_cache + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +#Ipython Notebook +.ipynb_checkpoints diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/.gitlab-ci.yml b/samples/openapi3/client/3_0_3_unit_test/python-experimental/.gitlab-ci.yml new file mode 100644 index 000000000000..22e33bfaa20a --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/.gitlab-ci.yml @@ -0,0 +1,24 @@ +# ref: https://docs.gitlab.com/ee/ci/README.html + +stages: + - test + +.tests: + stage: test + script: + - pip install -r requirements.txt + - pip install -r test-requirements.txt + - pytest --cov=unit_test_api + +test-3.5: + extends: .tests + image: python:3.5-alpine +test-3.6: + extends: .tests + image: python:3.6-alpine +test-3.7: + extends: .tests + image: python:3.7-alpine +test-3.8: + extends: .tests + image: python:3.8-alpine diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/.openapi-generator-ignore b/samples/openapi3/client/3_0_3_unit_test/python-experimental/.openapi-generator-ignore new file mode 100644 index 000000000000..7484ee590a38 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/.openapi-generator/FILES b/samples/openapi3/client/3_0_3_unit_test/python-experimental/.openapi-generator/FILES new file mode 100644 index 000000000000..6fa4115e71a0 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/.openapi-generator/FILES @@ -0,0 +1,33 @@ +.gitignore +.gitlab-ci.yml +.travis.yml +README.md +docs/ArrayTypeMatchesArrays.md +docs/BooleanTypeMatchesBooleans.md +docs/IntegerTypeMatchesIntegers.md +docs/NullTypeMatchesOnlyTheNullObject.md +docs/NumberTypeMatchesNumbers.md +docs/StringTypeMatchesStrings.md +git_push.sh +requirements.txt +setup.cfg +setup.py +test-requirements.txt +test/__init__.py +tox.ini +unit_test_api/__init__.py +unit_test_api/api/__init__.py +unit_test_api/api_client.py +unit_test_api/apis/__init__.py +unit_test_api/configuration.py +unit_test_api/exceptions.py +unit_test_api/model/__init__.py +unit_test_api/model/array_type_matches_arrays.py +unit_test_api/model/boolean_type_matches_booleans.py +unit_test_api/model/integer_type_matches_integers.py +unit_test_api/model/null_type_matches_only_the_null_object.py +unit_test_api/model/number_type_matches_numbers.py +unit_test_api/model/string_type_matches_strings.py +unit_test_api/models/__init__.py +unit_test_api/rest.py +unit_test_api/schemas.py diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/.openapi-generator/VERSION b/samples/openapi3/client/3_0_3_unit_test/python-experimental/.openapi-generator/VERSION new file mode 100644 index 000000000000..717311e32e3c --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/.openapi-generator/VERSION @@ -0,0 +1 @@ +unset \ No newline at end of file diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/.travis.yml b/samples/openapi3/client/3_0_3_unit_test/python-experimental/.travis.yml new file mode 100644 index 000000000000..a0d79cf14a9f --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/.travis.yml @@ -0,0 +1,13 @@ +# ref: https://docs.travis-ci.com/user/languages/python +language: python +python: + - "3.5" + - "3.6" + - "3.7" + - "3.8" +# command to install dependencies +install: + - "pip install -r requirements.txt" + - "pip install -r test-requirements.txt" +# command to run tests +script: pytest --cov=unit_test_api diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/Makefile b/samples/openapi3/client/3_0_3_unit_test/python-experimental/Makefile new file mode 100644 index 000000000000..863c380ebef9 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/Makefile @@ -0,0 +1,16 @@ +REQUIREMENTS_FILE=dev-requirements.txt +REQUIREMENTS_OUT=dev-requirements.txt.log +SETUP_OUT=*.egg-info +VENV=venv + +clean: + rm -rf $(REQUIREMENTS_OUT) + rm -rf $(SETUP_OUT) + rm -rf $(VENV) + rm -rf .tox + rm -rf .coverage + find . -name "*.py[oc]" -delete + find . -name "__pycache__" -delete + +test: clean + bash ./test_python.sh \ No newline at end of file diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/README.md b/samples/openapi3/client/3_0_3_unit_test/python-experimental/README.md new file mode 100644 index 000000000000..f0a363fdef85 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/README.md @@ -0,0 +1,96 @@ +# unit-test-api +sample spec for testing openapi functionality, built from json schema tests for draft6 + +This Python package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: + +- API version: 0.0.1 +- Package version: 1.0.0 +- Build package: org.openapitools.codegen.languages.PythonExperimentalClientCodegen + +## Requirements. + +Python >=3.9 +v3.9 is needed so one can combine classmethod and property decorators to define +object schema properties as classes + +## Installation & Usage +### pip install + +If the python package is hosted on a repository, you can install directly using: + +```sh +pip install git+https://github.com/GIT_USER_ID/GIT_REPO_ID.git +``` +(you may need to run `pip` with root permission: `sudo pip install git+https://github.com/GIT_USER_ID/GIT_REPO_ID.git`) + +Then import the package: +```python +import unit_test_api +``` + +### Setuptools + +Install via [Setuptools](http://pypi.python.org/pypi/setuptools). + +```sh +python setup.py install --user +``` +(or `sudo python setup.py install` to install the package for all users) + +Then import the package: +```python +import unit_test_api +``` + +## Getting Started + +Please follow the [installation procedure](#installation--usage) and then run the following: + +```python + +import time +import unit_test_api +from pprint import pprint +``` + +## Documentation for API Endpoints + +All URIs are relative to *http://localhost* + +Class | Method | HTTP request | Description +------------ | ------------- | ------------- | ------------- + +## Documentation For Models + + - [ArrayTypeMatchesArrays](docs/ArrayTypeMatchesArrays.md) + - [BooleanTypeMatchesBooleans](docs/BooleanTypeMatchesBooleans.md) + - [IntegerTypeMatchesIntegers](docs/IntegerTypeMatchesIntegers.md) + - [NullTypeMatchesOnlyTheNullObject](docs/NullTypeMatchesOnlyTheNullObject.md) + - [NumberTypeMatchesNumbers](docs/NumberTypeMatchesNumbers.md) + - [StringTypeMatchesStrings](docs/StringTypeMatchesStrings.md) + +## Documentation For Authorization + + All endpoints do not require authorization. + +## Author + + +## Notes for Large OpenAPI documents +If the OpenAPI document is large, imports in unit_test_api.apis and unit_test_api.models may fail with a +RecursionError indicating the maximum recursion limit has been exceeded. In that case, there are a couple of solutions: + +Solution 1: +Use specific imports for apis and models like: +- `from unit_test_api.api.default_api import DefaultApi` +- `from unit_test_api.model.pet import Pet` + +Solution 1: +Before importing the package, adjust the maximum recursion limit as shown below: +``` +import sys +sys.setrecursionlimit(1500) +import unit_test_api +from unit_test_api.apis import * +from unit_test_api.models import * +``` diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/dev-requirements.txt b/samples/openapi3/client/3_0_3_unit_test/python-experimental/dev-requirements.txt new file mode 100644 index 000000000000..ccdfca629494 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/dev-requirements.txt @@ -0,0 +1,2 @@ +tox +flake8 diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/docs/ArrayTypeMatchesArrays.md b/samples/openapi3/client/3_0_3_unit_test/python-experimental/docs/ArrayTypeMatchesArrays.md new file mode 100644 index 000000000000..bf72f2c35bcf --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/docs/ArrayTypeMatchesArrays.md @@ -0,0 +1,8 @@ +# ArrayTypeMatchesArrays + +Type | Description | Notes +------------- | ------------- | ------------- +**[bool, date, datetime, dict, float, int, list, str, none_type]** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/docs/BooleanTypeMatchesBooleans.md b/samples/openapi3/client/3_0_3_unit_test/python-experimental/docs/BooleanTypeMatchesBooleans.md new file mode 100644 index 000000000000..fa68a60fd2b3 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/docs/BooleanTypeMatchesBooleans.md @@ -0,0 +1,8 @@ +# BooleanTypeMatchesBooleans + +Type | Description | Notes +------------- | ------------- | ------------- +**bool** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/docs/IntegerTypeMatchesIntegers.md b/samples/openapi3/client/3_0_3_unit_test/python-experimental/docs/IntegerTypeMatchesIntegers.md new file mode 100644 index 000000000000..141e17fd9ae1 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/docs/IntegerTypeMatchesIntegers.md @@ -0,0 +1,8 @@ +# IntegerTypeMatchesIntegers + +Type | Description | Notes +------------- | ------------- | ------------- +**int** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/docs/NullTypeMatchesOnlyTheNullObject.md b/samples/openapi3/client/3_0_3_unit_test/python-experimental/docs/NullTypeMatchesOnlyTheNullObject.md new file mode 100644 index 000000000000..54a530d1d338 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/docs/NullTypeMatchesOnlyTheNullObject.md @@ -0,0 +1,8 @@ +# NullTypeMatchesOnlyTheNullObject + +Type | Description | Notes +------------- | ------------- | ------------- +**none_type** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/docs/NumberTypeMatchesNumbers.md b/samples/openapi3/client/3_0_3_unit_test/python-experimental/docs/NumberTypeMatchesNumbers.md new file mode 100644 index 000000000000..8348aeef2bc5 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/docs/NumberTypeMatchesNumbers.md @@ -0,0 +1,8 @@ +# NumberTypeMatchesNumbers + +Type | Description | Notes +------------- | ------------- | ------------- +**float** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/docs/StringTypeMatchesStrings.md b/samples/openapi3/client/3_0_3_unit_test/python-experimental/docs/StringTypeMatchesStrings.md new file mode 100644 index 000000000000..cd1125671ab7 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/docs/StringTypeMatchesStrings.md @@ -0,0 +1,8 @@ +# StringTypeMatchesStrings + +Type | Description | Notes +------------- | ------------- | ------------- +**str** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/git_push.sh b/samples/openapi3/client/3_0_3_unit_test/python-experimental/git_push.sh new file mode 100644 index 000000000000..ced3be2b0c7b --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/git_push.sh @@ -0,0 +1,58 @@ +#!/bin/sh +# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ +# +# Usage example: /bin/sh ./git_push.sh wing328 openapi-pestore-perl "minor update" "gitlab.com" + +git_user_id=$1 +git_repo_id=$2 +release_note=$3 +git_host=$4 + +if [ "$git_host" = "" ]; then + git_host="github.com" + echo "[INFO] No command line input provided. Set \$git_host to $git_host" +fi + +if [ "$git_user_id" = "" ]; then + git_user_id="GIT_USER_ID" + echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" +fi + +if [ "$git_repo_id" = "" ]; then + git_repo_id="GIT_REPO_ID" + echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" +fi + +if [ "$release_note" = "" ]; then + release_note="Minor update" + echo "[INFO] No command line input provided. Set \$release_note to $release_note" +fi + +# Initialize the local directory as a Git repository +git init + +# Adds the files in the local repository and stages them for commit. +git add . + +# Commits the tracked changes and prepares them to be pushed to a remote repository. +git commit -m "$release_note" + +# Sets the new remote +git_remote=`git remote` +if [ "$git_remote" = "" ]; then # git remote not defined + + if [ "$GIT_TOKEN" = "" ]; then + echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." + git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git + else + git remote add origin https://${git_user_id}:${GIT_TOKEN}@${git_host}/${git_user_id}/${git_repo_id}.git + fi + +fi + +git pull origin master + +# Pushes (Forces) the changes in the local repository up to the remote repository +echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" +git push origin master 2>&1 | grep -v 'To https' + diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/pom.xml b/samples/openapi3/client/3_0_3_unit_test/python-experimental/pom.xml new file mode 100644 index 000000000000..5352ff0ac54b --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/pom.xml @@ -0,0 +1,46 @@ + + 4.0.0 + org.openapitools + PythonExperimental303UnitTests + pom + 1.0-SNAPSHOT + Python Experimental OpenAPI3 Unit Test Api Client + + + + maven-dependency-plugin + + + package + + copy-dependencies + + + ${project.build.directory} + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.2.1 + + + test + integration-test + + exec + + + make + + test + + + + + + + + \ No newline at end of file diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/requirements.txt b/samples/openapi3/client/3_0_3_unit_test/python-experimental/requirements.txt new file mode 100644 index 000000000000..c9227e58a1be --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/requirements.txt @@ -0,0 +1,5 @@ +certifi >= 14.05.14 +frozendict >= 2.0.3 +python_dateutil >= 2.5.3 +setuptools >= 21.0.0 +urllib3 >= 1.15.1 diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/setup.cfg b/samples/openapi3/client/3_0_3_unit_test/python-experimental/setup.cfg new file mode 100644 index 000000000000..11433ee875ab --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/setup.cfg @@ -0,0 +1,2 @@ +[flake8] +max-line-length=99 diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/setup.py b/samples/openapi3/client/3_0_3_unit_test/python-experimental/setup.py new file mode 100644 index 000000000000..bac2d323981f --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/setup.py @@ -0,0 +1,45 @@ +# coding: utf-8 + +""" + openapi 3.0.3 sample spec + + sample spec for testing openapi functionality, built from json schema tests for draft6 # noqa: E501 + + The version of the OpenAPI document: 0.0.1 + Generated by: https://openapi-generator.tech +""" + +from setuptools import setup, find_packages # noqa: H301 + +NAME = "unit-test-api" +VERSION = "1.0.0" +# To install the library, run the following +# +# python setup.py install +# +# prerequisite: setuptools +# http://pypi.python.org/pypi/setuptools + +REQUIRES = [ + "urllib3 >= 1.15", + "certifi", + "python-dateutil", + "frozendict >= 2.0.3", +] + +setup( + name=NAME, + version=VERSION, + description="openapi 3.0.3 sample spec", + author="OpenAPI Generator community", + author_email="team@openapitools.org", + url="", + keywords=["OpenAPI", "OpenAPI-Generator", "openapi 3.0.3 sample spec"], + python_requires=">=3.9", + install_requires=REQUIRES, + packages=find_packages(exclude=["test", "tests"]), + include_package_data=True, + long_description="""\ + sample spec for testing openapi functionality, built from json schema tests for draft6 # noqa: E501 + """ +) diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/test-requirements.txt b/samples/openapi3/client/3_0_3_unit_test/python-experimental/test-requirements.txt new file mode 100644 index 000000000000..2d88b0341921 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/test-requirements.txt @@ -0,0 +1,3 @@ +pytest~=4.6.7 # needed for python 3.4 +pytest-cov>=2.8.1 +pytest-randomly==1.2.3 # needed for python 3.4 diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/test/__init__.py b/samples/openapi3/client/3_0_3_unit_test/python-experimental/test/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/test/test_array_type_matches_arrays.py b/samples/openapi3/client/3_0_3_unit_test/python-experimental/test/test_array_type_matches_arrays.py new file mode 100644 index 000000000000..47e27f0ea522 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/test/test_array_type_matches_arrays.py @@ -0,0 +1,73 @@ +# coding: utf-8 + +""" + openapi 3.0.3 sample spec + + sample spec for testing openapi functionality, built from json schema tests for draft6 # noqa: E501 + + The version of the OpenAPI document: 0.0.1 + Generated by: https://openapi-generator.tech +""" + +import unittest + +import unit_test_api +from unit_test_api.model.array_type_matches_arrays import ArrayTypeMatchesArrays + + +class TestArrayTypeMatchesArrays(unittest.TestCase): + """ArrayTypeMatchesArrays unit test stubs""" + + def test_a_float_is_not_an_array_fails(self): + # a float is not an array + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + ArrayTypeMatchesArrays( + 1.1 + ) + + def test_a_boolean_is_not_an_array_fails(self): + # a boolean is not an array + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + ArrayTypeMatchesArrays( + True + ) + + def test_null_is_not_an_array_fails(self): + # null is not an array + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + ArrayTypeMatchesArrays( + None + ) + + def test_an_object_is_not_an_array_fails(self): + # an object is not an array + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + ArrayTypeMatchesArrays( + { + } + ) + + def test_a_string_is_not_an_array_fails(self): + # a string is not an array + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + ArrayTypeMatchesArrays( + "foo" + ) + + def test_an_array_is_an_array_passes(self): + # an array is an array + ArrayTypeMatchesArrays( + [ + ] + ) + + def test_an_integer_is_not_an_array_fails(self): + # an integer is not an array + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + ArrayTypeMatchesArrays( + 1 + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/test/test_boolean_type_matches_booleans.py b/samples/openapi3/client/3_0_3_unit_test/python-experimental/test/test_boolean_type_matches_booleans.py new file mode 100644 index 000000000000..6e6fab281e72 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/test/test_boolean_type_matches_booleans.py @@ -0,0 +1,93 @@ +# coding: utf-8 + +""" + openapi 3.0.3 sample spec + + sample spec for testing openapi functionality, built from json schema tests for draft6 # noqa: E501 + + The version of the OpenAPI document: 0.0.1 + Generated by: https://openapi-generator.tech +""" + +import unittest + +import unit_test_api +from unit_test_api.model.boolean_type_matches_booleans import BooleanTypeMatchesBooleans + + +class TestBooleanTypeMatchesBooleans(unittest.TestCase): + """BooleanTypeMatchesBooleans unit test stubs""" + + def test_an_empty_string_is_not_a_boolean_fails(self): + # an empty string is not a boolean + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + BooleanTypeMatchesBooleans( + "" + ) + + def test_a_float_is_not_a_boolean_fails(self): + # a float is not a boolean + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + BooleanTypeMatchesBooleans( + 1.1 + ) + + def test_null_is_not_a_boolean_fails(self): + # null is not a boolean + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + BooleanTypeMatchesBooleans( + None + ) + + def test_zero_is_not_a_boolean_fails(self): + # zero is not a boolean + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + BooleanTypeMatchesBooleans( + 0 + ) + + def test_an_array_is_not_a_boolean_fails(self): + # an array is not a boolean + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + BooleanTypeMatchesBooleans( + [ + ] + ) + + def test_a_string_is_not_a_boolean_fails(self): + # a string is not a boolean + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + BooleanTypeMatchesBooleans( + "foo" + ) + + def test_false_is_a_boolean_passes(self): + # false is a boolean + BooleanTypeMatchesBooleans( + False + ) + + def test_an_integer_is_not_a_boolean_fails(self): + # an integer is not a boolean + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + BooleanTypeMatchesBooleans( + 1 + ) + + def test_true_is_a_boolean_passes(self): + # true is a boolean + BooleanTypeMatchesBooleans( + True + ) + + def test_an_object_is_not_a_boolean_fails(self): + # an object is not a boolean + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + BooleanTypeMatchesBooleans( + { + } + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/test/test_integer_type_matches_integers.py b/samples/openapi3/client/3_0_3_unit_test/python-experimental/test/test_integer_type_matches_integers.py new file mode 100644 index 000000000000..b118d36ab04b --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/test/test_integer_type_matches_integers.py @@ -0,0 +1,86 @@ +# coding: utf-8 + +""" + openapi 3.0.3 sample spec + + sample spec for testing openapi functionality, built from json schema tests for draft6 # noqa: E501 + + The version of the OpenAPI document: 0.0.1 + Generated by: https://openapi-generator.tech +""" + +import unittest + +import unit_test_api +from unit_test_api.model.integer_type_matches_integers import IntegerTypeMatchesIntegers + + +class TestIntegerTypeMatchesIntegers(unittest.TestCase): + """IntegerTypeMatchesIntegers unit test stubs""" + + def test_an_object_is_not_an_integer_fails(self): + # an object is not an integer + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + IntegerTypeMatchesIntegers( + { + } + ) + + def test_a_string_is_not_an_integer_fails(self): + # a string is not an integer + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + IntegerTypeMatchesIntegers( + "foo" + ) + + def test_null_is_not_an_integer_fails(self): + # null is not an integer + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + IntegerTypeMatchesIntegers( + None + ) + + def test_a_float_with_zero_fractional_part_is_an_integer_passes(self): + # a float with zero fractional part is an integer + IntegerTypeMatchesIntegers( + 1.0 + ) + + def test_a_float_is_not_an_integer_fails(self): + # a float is not an integer + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + IntegerTypeMatchesIntegers( + 1.1 + ) + + def test_a_boolean_is_not_an_integer_fails(self): + # a boolean is not an integer + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + IntegerTypeMatchesIntegers( + True + ) + + def test_an_integer_is_an_integer_passes(self): + # an integer is an integer + IntegerTypeMatchesIntegers( + 1 + ) + + def test_a_string_is_still_not_an_integer_even_if_it_looks_like_one_fails(self): + # a string is still not an integer, even if it looks like one + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + IntegerTypeMatchesIntegers( + "1" + ) + + def test_an_array_is_not_an_integer_fails(self): + # an array is not an integer + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + IntegerTypeMatchesIntegers( + [ + ] + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/test/test_null_type_matches_only_the_null_object.py b/samples/openapi3/client/3_0_3_unit_test/python-experimental/test/test_null_type_matches_only_the_null_object.py new file mode 100644 index 000000000000..bf8b35d150a1 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/test/test_null_type_matches_only_the_null_object.py @@ -0,0 +1,94 @@ +# coding: utf-8 + +""" + openapi 3.0.3 sample spec + + sample spec for testing openapi functionality, built from json schema tests for draft6 # noqa: E501 + + The version of the OpenAPI document: 0.0.1 + Generated by: https://openapi-generator.tech +""" + +import unittest + +import unit_test_api +from unit_test_api.model.null_type_matches_only_the_null_object import NullTypeMatchesOnlyTheNullObject + + +class TestNullTypeMatchesOnlyTheNullObject(unittest.TestCase): + """NullTypeMatchesOnlyTheNullObject unit test stubs""" + + def test_a_float_is_not_null_fails(self): + # a float is not null + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + NullTypeMatchesOnlyTheNullObject( + 1.1 + ) + + def test_an_object_is_not_null_fails(self): + # an object is not null + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + NullTypeMatchesOnlyTheNullObject( + { + } + ) + + def test_false_is_not_null_fails(self): + # false is not null + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + NullTypeMatchesOnlyTheNullObject( + False + ) + + def test_an_integer_is_not_null_fails(self): + # an integer is not null + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + NullTypeMatchesOnlyTheNullObject( + 1 + ) + + def test_true_is_not_null_fails(self): + # true is not null + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + NullTypeMatchesOnlyTheNullObject( + True + ) + + def test_zero_is_not_null_fails(self): + # zero is not null + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + NullTypeMatchesOnlyTheNullObject( + 0 + ) + + def test_an_empty_string_is_not_null_fails(self): + # an empty string is not null + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + NullTypeMatchesOnlyTheNullObject( + "" + ) + + def test_null_is_null_passes(self): + # null is null + NullTypeMatchesOnlyTheNullObject( + None + ) + + def test_an_array_is_not_null_fails(self): + # an array is not null + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + NullTypeMatchesOnlyTheNullObject( + [ + ] + ) + + def test_a_string_is_not_null_fails(self): + # a string is not null + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + NullTypeMatchesOnlyTheNullObject( + "foo" + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/test/test_number_type_matches_numbers.py b/samples/openapi3/client/3_0_3_unit_test/python-experimental/test/test_number_type_matches_numbers.py new file mode 100644 index 000000000000..24a4c25c842b --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/test/test_number_type_matches_numbers.py @@ -0,0 +1,85 @@ +# coding: utf-8 + +""" + openapi 3.0.3 sample spec + + sample spec for testing openapi functionality, built from json schema tests for draft6 # noqa: E501 + + The version of the OpenAPI document: 0.0.1 + Generated by: https://openapi-generator.tech +""" + +import unittest + +import unit_test_api +from unit_test_api.model.number_type_matches_numbers import NumberTypeMatchesNumbers + + +class TestNumberTypeMatchesNumbers(unittest.TestCase): + """NumberTypeMatchesNumbers unit test stubs""" + + def test_an_array_is_not_a_number_fails(self): + # an array is not a number + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + NumberTypeMatchesNumbers( + [ + ] + ) + + def test_null_is_not_a_number_fails(self): + # null is not a number + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + NumberTypeMatchesNumbers( + None + ) + + def test_an_object_is_not_a_number_fails(self): + # an object is not a number + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + NumberTypeMatchesNumbers( + { + } + ) + + def test_a_boolean_is_not_a_number_fails(self): + # a boolean is not a number + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + NumberTypeMatchesNumbers( + True + ) + + def test_a_float_is_a_number_passes(self): + # a float is a number + NumberTypeMatchesNumbers( + 1.1 + ) + + def test_a_string_is_still_not_a_number_even_if_it_looks_like_one_fails(self): + # a string is still not a number, even if it looks like one + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + NumberTypeMatchesNumbers( + "1" + ) + + def test_a_string_is_not_a_number_fails(self): + # a string is not a number + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + NumberTypeMatchesNumbers( + "foo" + ) + + def test_an_integer_is_a_number_passes(self): + # an integer is a number + NumberTypeMatchesNumbers( + 1 + ) + + def test_a_float_with_zero_fractional_part_is_a_number_and_an_integer_passes(self): + # a float with zero fractional part is a number (and an integer) + NumberTypeMatchesNumbers( + 1.0 + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/test/test_string_type_matches_strings.py b/samples/openapi3/client/3_0_3_unit_test/python-experimental/test/test_string_type_matches_strings.py new file mode 100644 index 000000000000..8dedf3982007 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/test/test_string_type_matches_strings.py @@ -0,0 +1,85 @@ +# coding: utf-8 + +""" + openapi 3.0.3 sample spec + + sample spec for testing openapi functionality, built from json schema tests for draft6 # noqa: E501 + + The version of the OpenAPI document: 0.0.1 + Generated by: https://openapi-generator.tech +""" + +import unittest + +import unit_test_api +from unit_test_api.model.string_type_matches_strings import StringTypeMatchesStrings + + +class TestStringTypeMatchesStrings(unittest.TestCase): + """StringTypeMatchesStrings unit test stubs""" + + def test_1_is_not_a_string_fails(self): + # 1 is not a string + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + StringTypeMatchesStrings( + 1 + ) + + def test_a_string_is_still_a_string_even_if_it_looks_like_a_number_passes(self): + # a string is still a string, even if it looks like a number + StringTypeMatchesStrings( + "1" + ) + + def test_an_empty_string_is_still_a_string_passes(self): + # an empty string is still a string + StringTypeMatchesStrings( + "" + ) + + def test_a_float_is_not_a_string_fails(self): + # a float is not a string + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + StringTypeMatchesStrings( + 1.1 + ) + + def test_an_object_is_not_a_string_fails(self): + # an object is not a string + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + StringTypeMatchesStrings( + { + } + ) + + def test_an_array_is_not_a_string_fails(self): + # an array is not a string + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + StringTypeMatchesStrings( + [ + ] + ) + + def test_a_boolean_is_not_a_string_fails(self): + # a boolean is not a string + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + StringTypeMatchesStrings( + True + ) + + def test_null_is_not_a_string_fails(self): + # null is not a string + with self.assertRaises((unit_test_api.ApiValueError, unit_test_api.ApiTypeError)): + StringTypeMatchesStrings( + None + ) + + def test_a_string_is_a_string_passes(self): + # a string is a string + StringTypeMatchesStrings( + "foo" + ) + + +if __name__ == '__main__': + unittest.main() diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/test_python.sh b/samples/openapi3/client/3_0_3_unit_test/python-experimental/test_python.sh new file mode 100755 index 000000000000..9728a9b53163 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/test_python.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +REQUIREMENTS_FILE=dev-requirements.txt +REQUIREMENTS_OUT=dev-requirements.txt.log +SETUP_OUT=*.egg-info +VENV=venv +DEACTIVE=false + +export LC_ALL=en_US.UTF-8 +export LANG=en_US.UTF-8 + +### set virtualenv +if [ -z "$VENVV" ]; then + python3 -m venv $VENV + source $VENV/bin/activate + DEACTIVE=true +fi + +### install dependencies +pip install -r $REQUIREMENTS_FILE | tee -a $REQUIREMENTS_OUT +### locally install the package, needed for pycharm problem checking +pip install -e . + +### run tests +tox || exit 1 + +### static analysis of code +#flake8 --show-source petstore_api/ + +### deactivate virtualenv +#if [ $DEACTIVE == true ]; then +# deactivate +#fi diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/tox.ini b/samples/openapi3/client/3_0_3_unit_test/python-experimental/tox.ini new file mode 100644 index 000000000000..e4093b56ea75 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/tox.ini @@ -0,0 +1,9 @@ +[tox] +envlist = py39 + +[testenv] +deps=-r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt + +commands= + pytest --cov=unit_test_api diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/__init__.py b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/__init__.py new file mode 100644 index 000000000000..5ca6e989be07 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/__init__.py @@ -0,0 +1,28 @@ +# coding: utf-8 + +# flake8: noqa + +""" + openapi 3.0.3 sample spec + + sample spec for testing openapi functionality, built from json schema tests for draft6 # noqa: E501 + + The version of the OpenAPI document: 0.0.1 + Generated by: https://openapi-generator.tech +""" + +__version__ = "1.0.0" + +# import ApiClient +from unit_test_api.api_client import ApiClient + +# import Configuration +from unit_test_api.configuration import Configuration + +# import exceptions +from unit_test_api.exceptions import OpenApiException +from unit_test_api.exceptions import ApiAttributeError +from unit_test_api.exceptions import ApiTypeError +from unit_test_api.exceptions import ApiValueError +from unit_test_api.exceptions import ApiKeyError +from unit_test_api.exceptions import ApiException diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/api/__init__.py b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/api/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/api_client.py b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/api_client.py new file mode 100644 index 000000000000..66e52cba296d --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/api_client.py @@ -0,0 +1,1420 @@ +# coding: utf-8 +""" + openapi 3.0.3 sample spec + + sample spec for testing openapi functionality, built from json schema tests for draft6 # noqa: E501 + + The version of the OpenAPI document: 0.0.1 + Generated by: https://openapi-generator.tech +""" + +from dataclasses import dataclass +from decimal import Decimal +import enum +import email +import json +import os +import io +import atexit +from multiprocessing.pool import ThreadPool +import re +import tempfile +import typing +import urllib3 +from urllib3._collections import HTTPHeaderDict +from urllib.parse import quote +from urllib3.fields import RequestField as RequestFieldBase + + +from unit_test_api import rest +from unit_test_api.configuration import Configuration +from unit_test_api.exceptions import ApiTypeError, ApiValueError +from unit_test_api.schemas import ( + NoneClass, + BoolClass, + Schema, + FileIO, + BinarySchema, + date, + datetime, + none_type, + frozendict, + Unset, + unset, +) + + +class RequestField(RequestFieldBase): + def __eq__(self, other): + if not isinstance(other, RequestField): + return False + return self.__dict__ == other.__dict__ + + +class JSONEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, str): + return str(obj) + elif isinstance(obj, float): + return float(obj) + elif isinstance(obj, int): + return int(obj) + elif isinstance(obj, Decimal): + if obj.as_tuple().exponent >= 0: + return int(obj) + return float(obj) + elif isinstance(obj, NoneClass): + return None + elif isinstance(obj, BoolClass): + return bool(obj) + elif isinstance(obj, (dict, frozendict)): + return {key: self.default(val) for key, val in obj.items()} + elif isinstance(obj, (list, tuple)): + return [self.default(item) for item in obj] + raise ApiValueError('Unable to prepare type {} for serialization'.format(obj.__class__.__name__)) + + +class ParameterInType(enum.Enum): + QUERY = 'query' + HEADER = 'header' + PATH = 'path' + COOKIE = 'cookie' + + +class ParameterStyle(enum.Enum): + MATRIX = 'matrix' + LABEL = 'label' + FORM = 'form' + SIMPLE = 'simple' + SPACE_DELIMITED = 'spaceDelimited' + PIPE_DELIMITED = 'pipeDelimited' + DEEP_OBJECT = 'deepObject' + + +class PrefixSeparatorIterator: + # A class to store prefixes and separators for rfc6570 expansions + + def __init__(self, prefix: str, separator: str): + self.prefix = prefix + self.separator = separator + self.first = True + if separator in {'.', '|', '%20'}: + item_separator = separator + else: + item_separator = ',' + self.item_separator = item_separator + + def __iter__(self): + return self + + def __next__(self): + if self.first: + self.first = False + return self.prefix + return self.separator + + +class ParameterSerializerBase: + @classmethod + def get_default_explode(cls, style: ParameterStyle) -> bool: + return False + + @staticmethod + def __ref6570_item_value(in_data: typing.Any, percent_encode: bool): + """ + Get representation if str/float/int/None/items in list/ values in dict + None is returned if an item is undefined, use cases are value= + - None + - [] + - {} + - [None, None None] + - {'a': None, 'b': None} + """ + if type(in_data) in {str, float, int}: + if percent_encode: + return quote(str(in_data)) + return str(in_data) + elif isinstance(in_data, none_type): + # ignored by the expansion process https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.1 + return None + elif isinstance(in_data, list) and not in_data: + # ignored by the expansion process https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.1 + return None + elif isinstance(in_data, dict) and not in_data: + # ignored by the expansion process https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.1 + return None + raise ApiValueError('Unable to generate a ref6570 item representation of {}'.format(in_data)) + + @staticmethod + def to_dict(name: str, value: str): + return {name: value} + + @classmethod + def ref6570_expansion( + cls, + variable_name: str, + in_data: typing.Any, + explode: bool, + percent_encode: bool, + prefix_separator_iterator: PrefixSeparatorIterator + ) -> str: + """ + Separator is for separate variables like dict with explode true, not for array item separation + """ + named_parameter_expansion = prefix_separator_iterator.separator in {'&', ';'} + var_name_piece = variable_name if named_parameter_expansion else '' + if type(in_data) in {str, float, int}: + item_value = cls.__ref6570_item_value(in_data, percent_encode) + if item_value is None: + return next(prefix_separator_iterator) + var_name_piece + elif item_value == '' and prefix_separator_iterator.separator == ';': + return next(prefix_separator_iterator) + var_name_piece + value_pair_equals = '=' if named_parameter_expansion else '' + return next(prefix_separator_iterator) + var_name_piece + value_pair_equals + item_value + elif isinstance(in_data, none_type): + # ignored by the expansion process https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.1 + return "" + elif isinstance(in_data, list): + item_values = [cls.__ref6570_item_value(v, percent_encode) for v in in_data] + item_values = [v for v in item_values if v is not None] + if not item_values: + # ignored by the expansion process https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.1 + return "" + value_pair_equals = '=' if named_parameter_expansion else '' + if not explode: + return ( + next(prefix_separator_iterator) + + var_name_piece + + value_pair_equals + + prefix_separator_iterator.item_separator.join(item_values) + ) + # exploded + return next(prefix_separator_iterator) + next(prefix_separator_iterator).join( + [var_name_piece + value_pair_equals + val for val in item_values] + ) + elif isinstance(in_data, dict): + in_data_transformed = {key: cls.__ref6570_item_value(val, percent_encode) for key, val in in_data.items()} + in_data_transformed = {key: val for key, val in in_data_transformed.items() if val is not None} + if not in_data_transformed: + # ignored by the expansion process https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.1 + return "" + value_pair_equals = '=' if named_parameter_expansion else '' + if not explode: + return ( + next(prefix_separator_iterator) + + var_name_piece + value_pair_equals + + prefix_separator_iterator.item_separator.join( + prefix_separator_iterator.item_separator.join( + item_pair + ) for item_pair in in_data_transformed.items() + ) + ) + # exploded + return next(prefix_separator_iterator) + next(prefix_separator_iterator).join( + [key + '=' + val for key, val in in_data_transformed.items()] + ) + # bool, bytes, etc + raise ApiValueError('Unable to generate a ref6570 representation of {}'.format(in_data)) + + +class StyleFormSerializer(ParameterSerializerBase): + @classmethod + def get_default_explode(cls, style: ParameterStyle) -> bool: + if style is ParameterStyle.FORM: + return True + return super().get_default_explode(style) + + def serialize_form( + self, + in_data: typing.Union[None, int, float, str, bool, dict, list], + name: str, + explode: bool, + percent_encode: bool, + prefix_separator_iterator: typing.Optional[PrefixSeparatorIterator] = None + ) -> str: + if prefix_separator_iterator is None: + prefix_separator_iterator = PrefixSeparatorIterator('?', '&') + return self.ref6570_expansion( + variable_name=name, + in_data=in_data, + explode=explode, + percent_encode=percent_encode, + prefix_separator_iterator=prefix_separator_iterator + ) + + +class StyleSimpleSerializer(ParameterSerializerBase): + + def serialize_simple( + self, + in_data: typing.Union[None, int, float, str, bool, dict, list], + name: str, + explode: bool, + percent_encode: bool + ) -> str: + prefix_separator_iterator = PrefixSeparatorIterator('', ',') + return self.ref6570_expansion( + variable_name=name, + in_data=in_data, + explode=explode, + percent_encode=percent_encode, + prefix_separator_iterator=prefix_separator_iterator + ) + + +@dataclass +class ParameterBase: + name: str + in_type: ParameterInType + required: bool + style: typing.Optional[ParameterStyle] + explode: typing.Optional[bool] + allow_reserved: typing.Optional[bool] + schema: typing.Optional[typing.Type[Schema]] + content: typing.Optional[typing.Dict[str, typing.Type[Schema]]] + + __style_to_in_type = { + ParameterStyle.MATRIX: {ParameterInType.PATH}, + ParameterStyle.LABEL: {ParameterInType.PATH}, + ParameterStyle.FORM: {ParameterInType.QUERY, ParameterInType.COOKIE}, + ParameterStyle.SIMPLE: {ParameterInType.PATH, ParameterInType.HEADER}, + ParameterStyle.SPACE_DELIMITED: {ParameterInType.QUERY}, + ParameterStyle.PIPE_DELIMITED: {ParameterInType.QUERY}, + ParameterStyle.DEEP_OBJECT: {ParameterInType.QUERY}, + } + __in_type_to_default_style = { + ParameterInType.QUERY: ParameterStyle.FORM, + ParameterInType.PATH: ParameterStyle.SIMPLE, + ParameterInType.HEADER: ParameterStyle.SIMPLE, + ParameterInType.COOKIE: ParameterStyle.FORM, + } + __disallowed_header_names = {'Accept', 'Content-Type', 'Authorization'} + _json_encoder = JSONEncoder() + _json_content_type = 'application/json' + + @classmethod + def __verify_style_to_in_type(cls, style: typing.Optional[ParameterStyle], in_type: ParameterInType): + if style is None: + return + in_type_set = cls.__style_to_in_type[style] + if in_type not in in_type_set: + raise ValueError( + 'Invalid style and in_type combination. For style={} only in_type={} are allowed'.format( + style, in_type_set + ) + ) + + def __init__( + self, + name: str, + in_type: ParameterInType, + required: bool = False, + style: typing.Optional[ParameterStyle] = None, + explode: bool = False, + allow_reserved: typing.Optional[bool] = None, + schema: typing.Optional[typing.Type[Schema]] = None, + content: typing.Optional[typing.Dict[str, typing.Type[Schema]]] = None + ): + if schema is None and content is None: + raise ValueError('Value missing; Pass in either schema or content') + if schema and content: + raise ValueError('Too many values provided. Both schema and content were provided. Only one may be input') + if name in self.__disallowed_header_names and in_type is ParameterInType.HEADER: + raise ValueError('Invalid name, name may not be one of {}'.format(self.__disallowed_header_names)) + self.__verify_style_to_in_type(style, in_type) + if content is None and style is None: + style = self.__in_type_to_default_style[in_type] + if content is not None and in_type in self.__in_type_to_default_style and len(content) != 1: + raise ValueError('Invalid content length, content length must equal 1') + self.in_type = in_type + self.name = name + self.required = required + self.style = style + self.explode = explode + self.allow_reserved = allow_reserved + self.schema = schema + self.content = content + + @staticmethod + def _remove_empty_and_cast( + in_data: typing.Tuple[typing.Tuple[str, str]], + ) -> typing.Dict[str, str]: + data = tuple(t for t in in_data if t) + if not data: + return dict() + return dict(data) + + def _serialize_json( + self, + in_data: typing.Union[None, int, float, str, bool, dict, list] + ) -> str: + return json.dumps(in_data) + + +class PathParameter(ParameterBase, StyleSimpleSerializer): + + def __init__( + self, + name: str, + required: bool = False, + style: typing.Optional[ParameterStyle] = None, + explode: bool = False, + allow_reserved: typing.Optional[bool] = None, + schema: typing.Optional[typing.Type[Schema]] = None, + content: typing.Optional[typing.Dict[str, typing.Type[Schema]]] = None + ): + super().__init__( + name, + in_type=ParameterInType.PATH, + required=required, + style=style, + explode=explode, + allow_reserved=allow_reserved, + schema=schema, + content=content + ) + + def _serialize_label( + self, + in_data: typing.Union[None, int, float, str, bool, dict, list] + ) -> typing.Dict[str, str]: + prefix_separator_iterator = PrefixSeparatorIterator('.', '.') + value = self.ref6570_expansion( + variable_name=self.name, + in_data=in_data, + explode=self.explode, + percent_encode=True, + prefix_separator_iterator=prefix_separator_iterator + ) + return self.to_dict(self.name, value) + + def _serialize_matrix( + self, + in_data: typing.Union[None, int, float, str, bool, dict, list] + ) -> typing.Dict[str, str]: + prefix_separator_iterator = PrefixSeparatorIterator(';', ';') + value = self.ref6570_expansion( + variable_name=self.name, + in_data=in_data, + explode=self.explode, + percent_encode=True, + prefix_separator_iterator=prefix_separator_iterator + ) + return self.to_dict(self.name, value) + + def _serialize_simple( + self, + in_data: typing.Union[None, int, float, str, bool, dict, list], + ) -> typing.Dict[str, str]: + value = self.serialize_simple( + in_data=in_data, + name=self.name, + explode=self.explode, + percent_encode=True + ) + return self.to_dict(self.name, value) + + def serialize( + self, + in_data: typing.Union[ + Schema, Decimal, int, float, str, date, datetime, None, bool, list, tuple, dict, frozendict] + ) -> typing.Dict[str, str]: + if self.schema: + cast_in_data = self.schema(in_data) + cast_in_data = self._json_encoder.default(cast_in_data) + """ + simple -> path + path: + returns path_params: dict + label -> path + returns path_params + matrix -> path + returns path_params + """ + if self.style: + if self.style is ParameterStyle.SIMPLE: + return self._serialize_simple(cast_in_data) + elif self.style is ParameterStyle.LABEL: + return self._serialize_label(cast_in_data) + elif self.style is ParameterStyle.MATRIX: + return self._serialize_matrix(cast_in_data) + # self.content will be length one + for content_type, schema in self.content.items(): + cast_in_data = schema(in_data) + cast_in_data = self._json_encoder.default(cast_in_data) + if content_type == self._json_content_type: + value = self._serialize_json(cast_in_data) + return self.to_dict(self.name, value) + raise NotImplementedError('Serialization of {} has not yet been implemented'.format(content_type)) + + +class QueryParameter(ParameterBase, StyleFormSerializer): + + def __init__( + self, + name: str, + required: bool = False, + style: typing.Optional[ParameterStyle] = None, + explode: typing.Optional[bool] = None, + allow_reserved: typing.Optional[bool] = None, + schema: typing.Optional[typing.Type[Schema]] = None, + content: typing.Optional[typing.Dict[str, typing.Type[Schema]]] = None + ): + used_style = ParameterStyle.FORM if style is None and content is None and schema else style + used_explode = self.get_default_explode(used_style) if explode is None else explode + + super().__init__( + name, + in_type=ParameterInType.QUERY, + required=required, + style=used_style, + explode=used_explode, + allow_reserved=allow_reserved, + schema=schema, + content=content + ) + + def __serialize_space_delimited( + self, + in_data: typing.Union[None, int, float, str, bool, dict, list], + prefix_separator_iterator: typing.Optional[PrefixSeparatorIterator] + ) -> typing.Dict[str, str]: + if prefix_separator_iterator is None: + prefix_separator_iterator = self.get_prefix_separator_iterator() + value = self.ref6570_expansion( + variable_name=self.name, + in_data=in_data, + explode=self.explode, + percent_encode=True, + prefix_separator_iterator=prefix_separator_iterator + ) + return self.to_dict(self.name, value) + + def __serialize_pipe_delimited( + self, + in_data: typing.Union[None, int, float, str, bool, dict, list], + prefix_separator_iterator: typing.Optional[PrefixSeparatorIterator] + ) -> typing.Dict[str, str]: + if prefix_separator_iterator is None: + prefix_separator_iterator = self.get_prefix_separator_iterator() + value = self.ref6570_expansion( + variable_name=self.name, + in_data=in_data, + explode=self.explode, + percent_encode=True, + prefix_separator_iterator=prefix_separator_iterator + ) + return self.to_dict(self.name, value) + + def __serialize_form( + self, + in_data: typing.Union[None, int, float, str, bool, dict, list], + prefix_separator_iterator: typing.Optional[PrefixSeparatorIterator] + ) -> typing.Dict[str, str]: + if prefix_separator_iterator is None: + prefix_separator_iterator = self.get_prefix_separator_iterator() + value = self.serialize_form( + in_data, + name=self.name, + explode=self.explode, + percent_encode=True, + prefix_separator_iterator=prefix_separator_iterator + ) + return self.to_dict(self.name, value) + + def get_prefix_separator_iterator(self) -> typing.Optional[PrefixSeparatorIterator]: + if not self.schema: + return None + if self.style is ParameterStyle.FORM: + return PrefixSeparatorIterator('?', '&') + elif self.style is ParameterStyle.SPACE_DELIMITED: + return PrefixSeparatorIterator('', '%20') + elif self.style is ParameterStyle.PIPE_DELIMITED: + return PrefixSeparatorIterator('', '|') + + def serialize( + self, + in_data: typing.Union[ + Schema, Decimal, int, float, str, date, datetime, None, bool, list, tuple, dict, frozendict], + prefix_separator_iterator: typing.Optional[PrefixSeparatorIterator] = None + ) -> typing.Dict[str, str]: + if self.schema: + cast_in_data = self.schema(in_data) + cast_in_data = self._json_encoder.default(cast_in_data) + """ + form -> query + query: + - GET/HEAD/DELETE: could use fields + - PUT/POST: must use urlencode to send parameters + returns fields: tuple + spaceDelimited -> query + returns fields + pipeDelimited -> query + returns fields + deepObject -> query, https://github.com/OAI/OpenAPI-Specification/issues/1706 + returns fields + """ + if self.style: + # TODO update query ones to omit setting values when [] {} or None is input + if self.style is ParameterStyle.FORM: + return self.__serialize_form(cast_in_data, prefix_separator_iterator) + elif self.style is ParameterStyle.SPACE_DELIMITED: + return self.__serialize_space_delimited(cast_in_data, prefix_separator_iterator) + elif self.style is ParameterStyle.PIPE_DELIMITED: + return self.__serialize_pipe_delimited(cast_in_data, prefix_separator_iterator) + # self.content will be length one + for content_type, schema in self.content.items(): + cast_in_data = schema(in_data) + cast_in_data = self._json_encoder.default(cast_in_data) + if content_type == self._json_content_type: + value = self._serialize_json(cast_in_data) + return self.to_dict(self.name, value) + raise NotImplementedError('Serialization of {} has not yet been implemented'.format(content_type)) + + +class CookieParameter(ParameterBase, StyleFormSerializer): + + def __init__( + self, + name: str, + required: bool = False, + style: typing.Optional[ParameterStyle] = None, + explode: typing.Optional[bool] = None, + allow_reserved: typing.Optional[bool] = None, + schema: typing.Optional[typing.Type[Schema]] = None, + content: typing.Optional[typing.Dict[str, typing.Type[Schema]]] = None + ): + used_style = ParameterStyle.FORM if style is None and content is None and schema else style + used_explode = self.get_default_explode(used_style) if explode is None else explode + + super().__init__( + name, + in_type=ParameterInType.COOKIE, + required=required, + style=used_style, + explode=used_explode, + allow_reserved=allow_reserved, + schema=schema, + content=content + ) + + def serialize( + self, + in_data: typing.Union[ + Schema, Decimal, int, float, str, date, datetime, None, bool, list, tuple, dict, frozendict] + ) -> typing.Dict[str, str]: + if self.schema: + cast_in_data = self.schema(in_data) + cast_in_data = self._json_encoder.default(cast_in_data) + """ + form -> cookie + returns fields: tuple + """ + if self.style: + """ + TODO add escaping of comma, space, equals + or turn encoding on + """ + value = self.serialize_form( + cast_in_data, + explode=self.explode, + name=self.name, + percent_encode=False, + prefix_separator_iterator=PrefixSeparatorIterator('', '&') + ) + return self.to_dict(self.name, value) + # self.content will be length one + for content_type, schema in self.content.items(): + cast_in_data = schema(in_data) + cast_in_data = self._json_encoder.default(cast_in_data) + if content_type == self._json_content_type: + value = self._serialize_json(cast_in_data) + return self.to_dict(self.name, value) + raise NotImplementedError('Serialization of {} has not yet been implemented'.format(content_type)) + + +class HeaderParameter(ParameterBase, StyleSimpleSerializer): + def __init__( + self, + name: str, + required: bool = False, + style: typing.Optional[ParameterStyle] = None, + explode: bool = False, + allow_reserved: typing.Optional[bool] = None, + schema: typing.Optional[typing.Type[Schema]] = None, + content: typing.Optional[typing.Dict[str, typing.Type[Schema]]] = None + ): + super().__init__( + name, + in_type=ParameterInType.HEADER, + required=required, + style=style, + explode=explode, + allow_reserved=allow_reserved, + schema=schema, + content=content + ) + + @staticmethod + def __to_headers(in_data: typing.Tuple[typing.Tuple[str, str], ...]) -> HTTPHeaderDict[str, str]: + data = tuple(t for t in in_data if t) + headers = HTTPHeaderDict() + if not data: + return headers + headers.extend(data) + return headers + + def _serialize_simple( + self, + in_data: typing.Union[None, int, float, str, bool, dict, list], + ) -> str: + return self.serialize_simple(in_data, self.name, self.explode, False) + + def serialize( + self, + in_data: typing.Union[ + Schema, Decimal, int, float, str, date, datetime, None, bool, list, tuple, dict, frozendict] + ) -> HTTPHeaderDict[str, str]: + if self.schema: + cast_in_data = self.schema(in_data) + cast_in_data = self._json_encoder.default(cast_in_data) + """ + simple -> header + headers: PoolManager needs a mapping, tuple is close + returns headers: dict + """ + if self.style: + value = self._serialize_simple(cast_in_data) + return self.__to_headers(((self.name, value),)) + # self.content will be length one + for content_type, schema in self.content.items(): + cast_in_data = schema(in_data) + cast_in_data = self._json_encoder.default(cast_in_data) + if content_type == self._json_content_type: + value = self._serialize_json(cast_in_data) + return self.__to_headers(((self.name, value),)) + raise NotImplementedError('Serialization of {} has not yet been implemented'.format(content_type)) + + +class Encoding: + def __init__( + self, + content_type: str, + headers: typing.Optional[typing.Dict[str, HeaderParameter]] = None, + style: typing.Optional[ParameterStyle] = None, + explode: bool = False, + allow_reserved: bool = False, + ): + self.content_type = content_type + self.headers = headers + self.style = style + self.explode = explode + self.allow_reserved = allow_reserved + + +@dataclass +class MediaType: + """ + Used to store request and response body schema information + encoding: + A map between a property name and its encoding information. + The key, being the property name, MUST exist in the schema as a property. + The encoding object SHALL only apply to requestBody objects when the media type is + multipart or application/x-www-form-urlencoded. + """ + schema: typing.Optional[typing.Type[Schema]] = None + encoding: typing.Optional[typing.Dict[str, Encoding]] = None + + +@dataclass +class ApiResponse: + response: urllib3.HTTPResponse + body: typing.Union[Unset, typing.Type[Schema]] + headers: typing.Union[Unset, typing.List[HeaderParameter]] + + def __init__( + self, + response: urllib3.HTTPResponse, + body: typing.Union[Unset, typing.Type[Schema]], + headers: typing.Union[Unset, typing.List[HeaderParameter]] + ): + """ + pycharm needs this to prevent 'Unexpected argument' warnings + """ + self.response = response + self.body = body + self.headers = headers + + +@dataclass +class ApiResponseWithoutDeserialization(ApiResponse): + response: urllib3.HTTPResponse + body: typing.Union[Unset, typing.Type[Schema]] = unset + headers: typing.Union[Unset, typing.List[HeaderParameter]] = unset + + +class JSONDetector: + @staticmethod + def content_type_is_json(content_type: str) -> bool: + """ + for when content_type strings also include charset info like: + application/json; charset=UTF-8 + """ + content_type_piece = content_type.split(';')[0] + if content_type_piece == 'application/json': + return True + return False + + +class OpenApiResponse(JSONDetector): + def __init__( + self, + response_cls: typing.Type[ApiResponse] = ApiResponse, + content: typing.Optional[typing.Dict[str, MediaType]] = None, + headers: typing.Optional[typing.List[HeaderParameter]] = None, + ): + self.headers = headers + if content is not None and len(content) == 0: + raise ValueError('Invalid value for content, the content dict must have >= 1 entry') + self.content = content + self.response_cls = response_cls + + @staticmethod + def __deserialize_json(response: urllib3.HTTPResponse) -> typing.Any: + # python must be >= 3.9 so we can pass in bytes into json.loads + return json.loads(response.data) + + @staticmethod + def __file_name_from_content_disposition(content_disposition: typing.Optional[str]) -> typing.Optional[str]: + if content_disposition is None: + return None + match = re.search('filename="(.+?)"', content_disposition) + if not match: + return None + return match.group(1) + + def __deserialize_application_octet_stream( + self, response: urllib3.HTTPResponse + ) -> typing.Union[bytes, io.BufferedReader]: + """ + urllib3 use cases: + 1. when preload_content=True (stream=False) then supports_chunked_reads is False and bytes are returned + 2. when preload_content=False (stream=True) then supports_chunked_reads is True and + a file will be written and returned + """ + if response.supports_chunked_reads(): + file_name = self.__file_name_from_content_disposition(response.headers.get('content-disposition')) + + if file_name is None: + _fd, path = tempfile.mkstemp() + else: + path = os.path.join(tempfile.gettempdir(), file_name) + # TODO get file_name from the filename at the end of the url if it exists + with open(path, 'wb') as new_file: + chunk_size = 1024 + while True: + data = response.read(chunk_size) + if not data: + break + new_file.write(data) + # release_conn is needed for streaming connections only + response.release_conn() + new_file = open(path, 'rb') + return new_file + else: + return response.data + + @staticmethod + def __deserialize_multipart_form_data( + response: urllib3.HTTPResponse + ) -> typing.Dict[str, typing.Any]: + msg = email.message_from_bytes(response.data) + return { + part.get_param("name", header="Content-Disposition"): part.get_payload( + decode=True + ).decode(part.get_content_charset()) + if part.get_content_charset() + else part.get_payload() + for part in msg.get_payload() + } + + def deserialize(self, response: urllib3.HTTPResponse, configuration: Configuration) -> ApiResponse: + content_type = response.getheader('content-type') + deserialized_body = unset + streamed = response.supports_chunked_reads() + + deserialized_headers = unset + if self.headers is not None: + # TODO add header deserialiation here + pass + + if self.content is not None: + if content_type not in self.content: + raise ApiValueError( + f'Invalid content_type={content_type} returned for response with ' + 'status_code={str(response.status)}' + ) + body_schema = self.content[content_type].schema + if body_schema is None: + # some specs do not define response content media type schemas + return self.response_cls( + response=response, + headers=deserialized_headers, + body=unset + ) + + if self.content_type_is_json(content_type): + body_data = self.__deserialize_json(response) + elif content_type == 'application/octet-stream': + body_data = self.__deserialize_application_octet_stream(response) + elif content_type.startswith('multipart/form-data'): + body_data = self.__deserialize_multipart_form_data(response) + content_type = 'multipart/form-data' + else: + raise NotImplementedError('Deserialization of {} has not yet been implemented'.format(content_type)) + deserialized_body = body_schema._from_openapi_data( + body_data, _configuration=configuration) + elif streamed: + response.release_conn() + + return self.response_cls( + response=response, + headers=deserialized_headers, + body=deserialized_body + ) + + +class ApiClient: + """Generic API client for OpenAPI client library builds. + + OpenAPI generic API client. This client handles the client- + server communication, and is invariant across implementations. Specifics of + the methods and models for each application are generated from the OpenAPI + templates. + + NOTE: This class is auto generated by OpenAPI Generator. + Ref: https://openapi-generator.tech + Do not edit the class manually. + + :param configuration: .Configuration object for this client + :param header_name: a header to pass when making calls to the API. + :param header_value: a header value to pass when making calls to + the API. + :param cookie: a cookie to include in the header when making calls + to the API + :param pool_threads: The number of threads to use for async requests + to the API. More threads means more concurrent API requests. + """ + + _pool = None + __json_encoder = JSONEncoder() + + def __init__( + self, + configuration: typing.Optional[Configuration] = None, + header_name: typing.Optional[str] = None, + header_value: typing.Optional[str] = None, + cookie: typing.Optional[str] = None, + pool_threads: int = 1 + ): + if configuration is None: + configuration = Configuration() + self.configuration = configuration + self.pool_threads = pool_threads + + self.rest_client = rest.RESTClientObject(configuration) + self.default_headers = {} + if header_name is not None: + self.default_headers[header_name] = header_value + self.cookie = cookie + # Set default User-Agent. + self.user_agent = 'OpenAPI-Generator/1.0.0/python' + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.close() + + def close(self): + if self._pool: + self._pool.close() + self._pool.join() + self._pool = None + if hasattr(atexit, 'unregister'): + atexit.unregister(self.close) + + @property + def pool(self): + """Create thread pool on first request + avoids instantiating unused threadpool for blocking clients. + """ + if self._pool is None: + atexit.register(self.close) + self._pool = ThreadPool(self.pool_threads) + return self._pool + + @property + def user_agent(self): + """User agent for this API client""" + return self.default_headers['User-Agent'] + + @user_agent.setter + def user_agent(self, value): + self.default_headers['User-Agent'] = value + + def set_default_header(self, header_name, header_value): + self.default_headers[header_name] = header_value + + def __call_api( + self, + resource_path: str, + method: str, + headers: typing.Optional[HTTPHeaderDict] = None, + body: typing.Optional[typing.Union[str, bytes]] = None, + fields: typing.Optional[typing.Tuple[typing.Tuple[str, str], ...]] = None, + auth_settings: typing.Optional[typing.List[str]] = None, + stream: bool = False, + timeout: typing.Optional[typing.Union[int, typing.Tuple]] = None, + host: typing.Optional[str] = None, + ) -> urllib3.HTTPResponse: + + # header parameters + headers = headers or {} + headers.update(self.default_headers) + if self.cookie: + headers['Cookie'] = self.cookie + + # auth setting + self.update_params_for_auth(headers, + auth_settings, resource_path, method, body) + + # request url + if host is None: + url = self.configuration.host + resource_path + else: + # use server/host defined in path or operation instead + url = host + resource_path + + # perform request and return response + response = self.request( + method, + url, + headers=headers, + fields=fields, + body=body, + stream=stream, + timeout=timeout, + ) + return response + + def call_api( + self, + resource_path: str, + method: str, + headers: typing.Optional[HTTPHeaderDict] = None, + body: typing.Optional[typing.Union[str, bytes]] = None, + fields: typing.Optional[typing.Tuple[typing.Tuple[str, str], ...]] = None, + auth_settings: typing.Optional[typing.List[str]] = None, + async_req: typing.Optional[bool] = None, + stream: bool = False, + timeout: typing.Optional[typing.Union[int, typing.Tuple]] = None, + host: typing.Optional[str] = None, + ) -> urllib3.HTTPResponse: + """Makes the HTTP request (synchronous) and returns deserialized data. + + To make an async_req request, set the async_req parameter. + + :param resource_path: Path to method endpoint. + :param method: Method to call. + :param headers: Header parameters to be + placed in the request header. + :param body: Request body. + :param fields: Request post form parameters, + for `application/x-www-form-urlencoded`, `multipart/form-data`. + :param auth_settings: Auth Settings names for the request. + :param async_req: execute request asynchronously + :type async_req: bool, optional TODO remove, unused + :param stream: if True, the urllib3.HTTPResponse object will + be returned without reading/decoding response + data. Also when True, if the openapi spec describes a file download, + the data will be written to a local filesystme file and the BinarySchema + instance will also inherit from FileSchema and FileIO + Default is False. + :type stream: bool, optional + :param timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + :param host: api endpoint host + :return: + If async_req parameter is True, + the request will be called asynchronously. + The method will return the request thread. + If parameter async_req is False or missing, + then the method will return the response directly. + """ + + if not async_req: + return self.__call_api( + resource_path, + method, + headers, + body, + fields, + auth_settings, + stream, + timeout, + host, + ) + + return self.pool.apply_async( + self.__call_api, + ( + resource_path, + method, + headers, + body, + json, + fields, + auth_settings, + stream, + timeout, + host, + ) + ) + + def request( + self, + method: str, + url: str, + headers: typing.Optional[HTTPHeaderDict] = None, + fields: typing.Optional[typing.Tuple[typing.Tuple[str, str], ...]] = None, + body: typing.Optional[typing.Union[str, bytes]] = None, + stream: bool = False, + timeout: typing.Optional[typing.Union[int, typing.Tuple]] = None, + ) -> urllib3.HTTPResponse: + """Makes the HTTP request using RESTClient.""" + if method == "GET": + return self.rest_client.GET(url, + stream=stream, + timeout=timeout, + headers=headers) + elif method == "HEAD": + return self.rest_client.HEAD(url, + stream=stream, + timeout=timeout, + headers=headers) + elif method == "OPTIONS": + return self.rest_client.OPTIONS(url, + headers=headers, + fields=fields, + stream=stream, + timeout=timeout, + body=body) + elif method == "POST": + return self.rest_client.POST(url, + headers=headers, + fields=fields, + stream=stream, + timeout=timeout, + body=body) + elif method == "PUT": + return self.rest_client.PUT(url, + headers=headers, + fields=fields, + stream=stream, + timeout=timeout, + body=body) + elif method == "PATCH": + return self.rest_client.PATCH(url, + headers=headers, + fields=fields, + stream=stream, + timeout=timeout, + body=body) + elif method == "DELETE": + return self.rest_client.DELETE(url, + headers=headers, + stream=stream, + timeout=timeout, + body=body) + else: + raise ApiValueError( + "http method must be `GET`, `HEAD`, `OPTIONS`," + " `POST`, `PATCH`, `PUT` or `DELETE`." + ) + + def update_params_for_auth(self, headers, auth_settings, + resource_path, method, body): + """Updates header and query params based on authentication setting. + + :param headers: Header parameters dict to be updated. + :param auth_settings: Authentication setting identifiers list. + :param resource_path: A string representation of the HTTP request resource path. + :param method: A string representation of the HTTP request method. + :param body: A object representing the body of the HTTP request. + The object type is the return value of _encoder.default(). + """ + if not auth_settings: + return + + for auth in auth_settings: + auth_setting = self.configuration.auth_settings().get(auth) + if auth_setting: + if auth_setting['in'] == 'cookie': + headers.add('Cookie', auth_setting['value']) + elif auth_setting['in'] == 'header': + if auth_setting['type'] != 'http-signature': + headers.add(auth_setting['key'], auth_setting['value']) + elif auth_setting['in'] == 'query': + """ TODO implement auth in query + need to pass in prefix_separator_iterator + and need to output resource_path with query params added + """ + raise ApiValueError("Auth in query not yet implemented") + else: + raise ApiValueError( + 'Authentication token must be in `query` or `header`' + ) + + +class Api: + """NOTE: This class is auto generated by OpenAPI Generator + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + + def __init__(self, api_client: typing.Optional[ApiClient] = None): + if api_client is None: + api_client = ApiClient() + self.api_client = api_client + + @staticmethod + def _verify_typed_dict_inputs(cls: typing.Type[typing.TypedDict], data: typing.Dict[str, typing.Any]): + """ + Ensures that: + - required keys are present + - additional properties are not input + - value stored under required keys do not have the value unset + Note: detailed value checking is done in schema classes + """ + missing_required_keys = [] + required_keys_with_unset_values = [] + for required_key in cls.__required_keys__: + if required_key not in data: + missing_required_keys.append(required_key) + continue + value = data[required_key] + if value is unset: + required_keys_with_unset_values.append(required_key) + if missing_required_keys: + raise ApiTypeError( + '{} missing {} required arguments: {}'.format( + cls.__name__, len(missing_required_keys), missing_required_keys + ) + ) + if required_keys_with_unset_values: + raise ApiValueError( + '{} contains invalid unset values for {} required keys: {}'.format( + cls.__name__, len(required_keys_with_unset_values), required_keys_with_unset_values + ) + ) + + disallowed_additional_keys = [] + for key in data: + if key in cls.__required_keys__ or key in cls.__optional_keys__: + continue + disallowed_additional_keys.append(key) + if disallowed_additional_keys: + raise ApiTypeError( + '{} got {} unexpected keyword arguments: {}'.format( + cls.__name__, len(disallowed_additional_keys), disallowed_additional_keys + ) + ) + + def get_host( + self, + operation_id: str, + servers: typing.Tuple[typing.Dict[str, str], ...] = tuple(), + host_index: typing.Optional[int] = None + ) -> typing.Optional[str]: + configuration = self.api_client.configuration + try: + if host_index is None: + index = configuration.server_operation_index.get( + operation_id, configuration.server_index + ) + else: + index = host_index + server_variables = configuration.server_operation_variables.get( + operation_id, configuration.server_variables + ) + host = configuration.get_host_from_settings( + index, variables=server_variables, servers=servers + ) + except IndexError: + if servers: + raise ApiValueError( + "Invalid host index. Must be 0 <= index < %s" % + len(servers) + ) + host = None + return host + + +class SerializedRequestBody(typing.TypedDict, total=False): + body: typing.Union[str, bytes] + fields: typing.Tuple[typing.Union[RequestField, tuple[str, str]], ...] + + +class RequestBody(StyleFormSerializer, JSONDetector): + """ + A request body parameter + content: content_type to MediaType Schema info + """ + __json_encoder = JSONEncoder() + + def __init__( + self, + content: typing.Dict[str, MediaType], + required: bool = False, + ): + self.required = required + if len(content) == 0: + raise ValueError('Invalid value for content, the content dict must have >= 1 entry') + self.content = content + + def __serialize_json( + self, + in_data: typing.Any + ) -> typing.Dict[str, bytes]: + in_data = self.__json_encoder.default(in_data) + json_str = json.dumps(in_data, separators=(",", ":"), ensure_ascii=False).encode( + "utf-8" + ) + return dict(body=json_str) + + @staticmethod + def __serialize_text_plain(in_data: typing.Any) -> typing.Dict[str, str]: + if isinstance(in_data, frozendict): + raise ValueError('Unable to serialize type frozendict to text/plain') + elif isinstance(in_data, tuple): + raise ValueError('Unable to serialize type tuple to text/plain') + elif isinstance(in_data, NoneClass): + raise ValueError('Unable to serialize type NoneClass to text/plain') + elif isinstance(in_data, BoolClass): + raise ValueError('Unable to serialize type BoolClass to text/plain') + return dict(body=str(in_data)) + + def __multipart_json_item(self, key: str, value: Schema) -> RequestField: + json_value = self.__json_encoder.default(value) + return RequestField(name=key, data=json.dumps(json_value), headers={'Content-Type': 'application/json'}) + + def __multipart_form_item(self, key: str, value: Schema) -> RequestField: + if isinstance(value, str): + return RequestField(name=key, data=str(value), headers={'Content-Type': 'text/plain'}) + elif isinstance(value, bytes): + return RequestField(name=key, data=value, headers={'Content-Type': 'application/octet-stream'}) + elif isinstance(value, FileIO): + request_field = RequestField( + name=key, + data=value.read(), + filename=os.path.basename(value.name), + headers={'Content-Type': 'application/octet-stream'} + ) + value.close() + return request_field + else: + return self.__multipart_json_item(key=key, value=value) + + def __serialize_multipart_form_data( + self, in_data: Schema + ) -> typing.Dict[str, typing.Tuple[RequestField, ...]]: + if not isinstance(in_data, frozendict): + raise ValueError(f'Unable to serialize {in_data} to multipart/form-data because it is not a dict of data') + """ + In a multipart/form-data request body, each schema property, or each element of a schema array property, + takes a section in the payload with an internal header as defined by RFC7578. The serialization strategy + for each property of a multipart/form-data request body can be specified in an associated Encoding Object. + + When passing in multipart types, boundaries MAY be used to separate sections of the content being + transferred – thus, the following default Content-Types are defined for multipart: + + If the (object) property is a primitive, or an array of primitive values, the default Content-Type is text/plain + If the property is complex, or an array of complex values, the default Content-Type is application/json + Question: how is the array of primitives encoded? + If the property is a type: string with a contentEncoding, the default Content-Type is application/octet-stream + """ + fields = [] + for key, value in in_data.items(): + if isinstance(value, tuple): + if value: + # values use explode = True, so the code makes a RequestField for each item with name=key + for item in value: + request_field = self.__multipart_form_item(key=key, value=item) + fields.append(request_field) + else: + # send an empty array as json because exploding will not send it + request_field = self.__multipart_json_item(key=key, value=value) + fields.append(request_field) + else: + request_field = self.__multipart_form_item(key=key, value=value) + fields.append(request_field) + + return dict(fields=tuple(fields)) + + def __serialize_application_octet_stream(self, in_data: BinarySchema) -> typing.Dict[str, bytes]: + if isinstance(in_data, bytes): + return dict(body=in_data) + # FileIO type + result = dict(body=in_data.read()) + in_data.close() + return result + + def __serialize_application_x_www_form_data( + self, in_data: typing.Any + ) -> SerializedRequestBody: + """ + POST submission of form data in body + """ + if not isinstance(in_data, frozendict): + raise ValueError( + f'Unable to serialize {in_data} to application/x-www-form-urlencoded because it is not a dict of data') + cast_in_data = self.__json_encoder.default(in_data) + value = self.serialize_form(cast_in_data, name='', explode=True, percent_encode=False) + return dict(body=value) + + def serialize( + self, in_data: typing.Any, content_type: str + ) -> SerializedRequestBody: + """ + If a str is returned then the result will be assigned to data when making the request + If a tuple is returned then the result will be used as fields input in encode_multipart_formdata + Return a tuple of + + The key of the return dict is + - body for application/json + - encode_multipart and fields for multipart/form-data + """ + media_type = self.content[content_type] + if isinstance(in_data, media_type.schema): + cast_in_data = in_data + elif isinstance(in_data, (dict, frozendict)) and in_data: + cast_in_data = media_type.schema(**in_data) + else: + cast_in_data = media_type.schema(in_data) + # TODO check for and use encoding if it exists + # and content_type is multipart or application/x-www-form-urlencoded + if self.content_type_is_json(content_type): + return self.__serialize_json(cast_in_data) + elif content_type == 'text/plain': + return self.__serialize_text_plain(cast_in_data) + elif content_type == 'multipart/form-data': + return self.__serialize_multipart_form_data(cast_in_data) + elif content_type == 'application/x-www-form-urlencoded': + return self.__serialize_application_x_www_form_data(cast_in_data) + elif content_type == 'application/octet-stream': + return self.__serialize_application_octet_stream(cast_in_data) + raise NotImplementedError('Serialization has not yet been implemented for {}'.format(content_type)) diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/apis/__init__.py b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/apis/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/configuration.py b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/configuration.py new file mode 100644 index 000000000000..80407ef4b449 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/configuration.py @@ -0,0 +1,440 @@ +# coding: utf-8 + +""" + openapi 3.0.3 sample spec + + sample spec for testing openapi functionality, built from json schema tests for draft6 # noqa: E501 + + The version of the OpenAPI document: 0.0.1 + Generated by: https://openapi-generator.tech +""" + +import copy +import logging +import multiprocessing +import sys +import urllib3 + +from http import client as http_client +from unit_test_api.exceptions import ApiValueError + + +JSON_SCHEMA_VALIDATION_KEYWORDS = { + 'multipleOf', 'maximum', 'exclusiveMaximum', + 'minimum', 'exclusiveMinimum', 'maxLength', + 'minLength', 'pattern', 'maxItems', 'minItems', + 'uniqueItems', 'maxProperties', 'minProperties', +} + +class Configuration(object): + """NOTE: This class is auto generated by OpenAPI Generator + + Ref: https://openapi-generator.tech + Do not edit the class manually. + + :param host: Base url + :param api_key: Dict to store API key(s). + Each entry in the dict specifies an API key. + The dict key is the name of the security scheme in the OAS specification. + The dict value is the API key secret. + :param api_key_prefix: Dict to store API prefix (e.g. Bearer) + The dict key is the name of the security scheme in the OAS specification. + The dict value is an API key prefix when generating the auth data. + :param username: Username for HTTP basic authentication + :param password: Password for HTTP basic authentication + :param discard_unknown_keys: Boolean value indicating whether to discard + unknown properties. A server may send a response that includes additional + properties that are not known by the client in the following scenarios: + 1. The OpenAPI document is incomplete, i.e. it does not match the server + implementation. + 2. The client was generated using an older version of the OpenAPI document + and the server has been upgraded since then. + If a schema in the OpenAPI document defines the additionalProperties attribute, + then all undeclared properties received by the server are injected into the + additional properties map. In that case, there are undeclared properties, and + nothing to discard. + :param disabled_client_side_validations (string): Comma-separated list of + JSON schema validation keywords to disable JSON schema structural validation + rules. The following keywords may be specified: multipleOf, maximum, + exclusiveMaximum, minimum, exclusiveMinimum, maxLength, minLength, pattern, + maxItems, minItems. + By default, the validation is performed for data generated locally by the client + and data received from the server, independent of any validation performed by + the server side. If the input data does not satisfy the JSON schema validation + rules specified in the OpenAPI document, an exception is raised. + If disabled_client_side_validations is set, structural validation is + disabled. This can be useful to troubleshoot data validation problem, such as + when the OpenAPI document validation rules do not match the actual API data + received by the server. + :param server_index: Index to servers configuration. + :param server_variables: Mapping with string values to replace variables in + templated server configuration. The validation of enums is performed for + variables with defined enum values before. + :param server_operation_index: Mapping from operation ID to an index to server + configuration. + :param server_operation_variables: Mapping from operation ID to a mapping with + string values to replace variables in templated server configuration. + The validation of enums is performed for variables with defined enum values before. + + """ + + _default = None + + def __init__(self, host=None, + api_key=None, api_key_prefix=None, + username=None, password=None, + discard_unknown_keys=False, + disabled_client_side_validations="", + server_index=None, server_variables=None, + server_operation_index=None, server_operation_variables=None, + ): + """Constructor + """ + self._base_path = "http://localhost" if host is None else host + """Default Base url + """ + self.server_index = 0 if server_index is None and host is None else server_index + self.server_operation_index = server_operation_index or {} + """Default server index + """ + self.server_variables = server_variables or {} + self.server_operation_variables = server_operation_variables or {} + """Default server variables + """ + self.temp_folder_path = None + """Temp file folder for downloading files + """ + # Authentication Settings + self.api_key = {} + if api_key: + self.api_key = api_key + """dict to store API key(s) + """ + self.api_key_prefix = {} + if api_key_prefix: + self.api_key_prefix = api_key_prefix + """dict to store API prefix (e.g. Bearer) + """ + self.refresh_api_key_hook = None + """function hook to refresh API key if expired + """ + self.username = username + """Username for HTTP basic authentication + """ + self.password = password + """Password for HTTP basic authentication + """ + self.discard_unknown_keys = discard_unknown_keys + self.disabled_client_side_validations = disabled_client_side_validations + self.logger = {} + """Logging Settings + """ + self.logger["package_logger"] = logging.getLogger("unit_test_api") + self.logger["urllib3_logger"] = logging.getLogger("urllib3") + self.logger_format = '%(asctime)s %(levelname)s %(message)s' + """Log format + """ + self.logger_stream_handler = None + """Log stream handler + """ + self.logger_file_handler = None + """Log file handler + """ + self.logger_file = None + """Debug file location + """ + self.debug = False + """Debug switch + """ + + self.verify_ssl = True + """SSL/TLS verification + Set this to false to skip verifying SSL certificate when calling API + from https server. + """ + self.ssl_ca_cert = None + """Set this to customize the certificate file to verify the peer. + """ + self.cert_file = None + """client certificate file + """ + self.key_file = None + """client key file + """ + self.assert_hostname = None + """Set this to True/False to enable/disable SSL hostname verification. + """ + + self.connection_pool_maxsize = multiprocessing.cpu_count() * 5 + """urllib3 connection pool's maximum number of connections saved + per pool. urllib3 uses 1 connection as default value, but this is + not the best value when you are making a lot of possibly parallel + requests to the same host, which is often the case here. + cpu_count * 5 is used as default value to increase performance. + """ + + self.proxy = None + """Proxy URL + """ + self.proxy_headers = None + """Proxy headers + """ + self.safe_chars_for_path_param = '' + """Safe chars for path_param + """ + self.retries = None + """Adding retries to override urllib3 default value 3 + """ + # Enable client side validation + self.client_side_validation = True + + # Options to pass down to the underlying urllib3 socket + self.socket_options = None + + def __deepcopy__(self, memo): + cls = self.__class__ + result = cls.__new__(cls) + memo[id(self)] = result + for k, v in self.__dict__.items(): + if k not in ('logger', 'logger_file_handler'): + setattr(result, k, copy.deepcopy(v, memo)) + # shallow copy of loggers + result.logger = copy.copy(self.logger) + # use setters to configure loggers + result.logger_file = self.logger_file + result.debug = self.debug + return result + + def __setattr__(self, name, value): + object.__setattr__(self, name, value) + if name == 'disabled_client_side_validations': + s = set(filter(None, value.split(','))) + for v in s: + if v not in JSON_SCHEMA_VALIDATION_KEYWORDS: + raise ApiValueError( + "Invalid keyword: '{0}''".format(v)) + self._disabled_client_side_validations = s + + @classmethod + def set_default(cls, default): + """Set default instance of configuration. + + It stores default configuration, which can be + returned by get_default_copy method. + + :param default: object of Configuration + """ + cls._default = copy.deepcopy(default) + + @classmethod + def get_default_copy(cls): + """Return new instance of configuration. + + This method returns newly created, based on default constructor, + object of Configuration class or returns a copy of default + configuration passed by the set_default method. + + :return: The configuration object. + """ + if cls._default is not None: + return copy.deepcopy(cls._default) + return Configuration() + + @property + def logger_file(self): + """The logger file. + + If the logger_file is None, then add stream handler and remove file + handler. Otherwise, add file handler and remove stream handler. + + :param value: The logger_file path. + :type: str + """ + return self.__logger_file + + @logger_file.setter + def logger_file(self, value): + """The logger file. + + If the logger_file is None, then add stream handler and remove file + handler. Otherwise, add file handler and remove stream handler. + + :param value: The logger_file path. + :type: str + """ + self.__logger_file = value + if self.__logger_file: + # If set logging file, + # then add file handler and remove stream handler. + self.logger_file_handler = logging.FileHandler(self.__logger_file) + self.logger_file_handler.setFormatter(self.logger_formatter) + for _, logger in self.logger.items(): + logger.addHandler(self.logger_file_handler) + + @property + def debug(self): + """Debug status + + :param value: The debug status, True or False. + :type: bool + """ + return self.__debug + + @debug.setter + def debug(self, value): + """Debug status + + :param value: The debug status, True or False. + :type: bool + """ + self.__debug = value + if self.__debug: + # if debug status is True, turn on debug logging + for _, logger in self.logger.items(): + logger.setLevel(logging.DEBUG) + # turn on http_client debug + http_client.HTTPConnection.debuglevel = 1 + else: + # if debug status is False, turn off debug logging, + # setting log level to default `logging.WARNING` + for _, logger in self.logger.items(): + logger.setLevel(logging.WARNING) + # turn off http_client debug + http_client.HTTPConnection.debuglevel = 0 + + @property + def logger_format(self): + """The logger format. + + The logger_formatter will be updated when sets logger_format. + + :param value: The format string. + :type: str + """ + return self.__logger_format + + @logger_format.setter + def logger_format(self, value): + """The logger format. + + The logger_formatter will be updated when sets logger_format. + + :param value: The format string. + :type: str + """ + self.__logger_format = value + self.logger_formatter = logging.Formatter(self.__logger_format) + + def get_api_key_with_prefix(self, identifier, alias=None): + """Gets API key (with prefix if set). + + :param identifier: The identifier of apiKey. + :param alias: The alternative identifier of apiKey. + :return: The token for api key authentication. + """ + if self.refresh_api_key_hook is not None: + self.refresh_api_key_hook(self) + key = self.api_key.get(identifier, self.api_key.get(alias) if alias is not None else None) + if key: + prefix = self.api_key_prefix.get(identifier) + if prefix: + return "%s %s" % (prefix, key) + else: + return key + + def get_basic_auth_token(self): + """Gets HTTP basic authentication header (string). + + :return: The token for basic HTTP authentication. + """ + username = "" + if self.username is not None: + username = self.username + password = "" + if self.password is not None: + password = self.password + return urllib3.util.make_headers( + basic_auth=username + ':' + password + ).get('authorization') + + def auth_settings(self): + """Gets Auth Settings dict for api client. + + :return: The Auth Settings information dict. + """ + auth = {} + return auth + + def to_debug_report(self): + """Gets the essential information for debugging. + + :return: The report for debugging. + """ + return "Python SDK Debug Report:\n"\ + "OS: {env}\n"\ + "Python Version: {pyversion}\n"\ + "Version of the API: 0.0.1\n"\ + "SDK Package Version: 1.0.0".\ + format(env=sys.platform, pyversion=sys.version) + + def get_host_settings(self): + """Gets an array of host settings + + :return: An array of host settings + """ + return [ + { + 'url': "", + 'description': "No description provided", + } + ] + + def get_host_from_settings(self, index, variables=None, servers=None): + """Gets host URL based on the index and variables + :param index: array index of the host settings + :param variables: hash of variable and the corresponding value + :param servers: an array of host settings or None + :return: URL based on host settings + """ + if index is None: + return self._base_path + + variables = {} if variables is None else variables + servers = self.get_host_settings() if servers is None else servers + + try: + server = servers[index] + except IndexError: + raise ValueError( + "Invalid index {0} when selecting the host settings. " + "Must be less than {1}".format(index, len(servers))) + + url = server['url'] + + # go through variables and replace placeholders + for variable_name, variable in server.get('variables', {}).items(): + used_value = variables.get( + variable_name, variable['default_value']) + + if 'enum_values' in variable \ + and used_value not in variable['enum_values']: + raise ValueError( + "The variable `{0}` in the host URL has invalid value " + "{1}. Must be {2}.".format( + variable_name, variables[variable_name], + variable['enum_values'])) + + url = url.replace("{" + variable_name + "}", used_value) + + return url + + @property + def host(self): + """Return generated host.""" + return self.get_host_from_settings(self.server_index, variables=self.server_variables) + + @host.setter + def host(self, value): + """Fix base path.""" + self._base_path = value + self.server_index = None diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/exceptions.py b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/exceptions.py new file mode 100644 index 000000000000..54a51c36ab47 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/exceptions.py @@ -0,0 +1,136 @@ +# coding: utf-8 + +""" + openapi 3.0.3 sample spec + + sample spec for testing openapi functionality, built from json schema tests for draft6 # noqa: E501 + + The version of the OpenAPI document: 0.0.1 + Generated by: https://openapi-generator.tech +""" + + +class OpenApiException(Exception): + """The base exception class for all OpenAPIExceptions""" + + +class ApiTypeError(OpenApiException, TypeError): + def __init__(self, msg, path_to_item=None, valid_classes=None, + key_type=None): + """ Raises an exception for TypeErrors + + Args: + msg (str): the exception message + + Keyword Args: + path_to_item (list): a list of keys an indices to get to the + current_item + None if unset + valid_classes (tuple): the primitive classes that current item + should be an instance of + None if unset + key_type (bool): False if our value is a value in a dict + True if it is a key in a dict + False if our item is an item in a list + None if unset + """ + self.path_to_item = path_to_item + self.valid_classes = valid_classes + self.key_type = key_type + full_msg = msg + if path_to_item: + full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) + super(ApiTypeError, self).__init__(full_msg) + + +class ApiValueError(OpenApiException, ValueError): + def __init__(self, msg, path_to_item=None): + """ + Args: + msg (str): the exception message + + Keyword Args: + path_to_item (list) the path to the exception in the + received_data dict. None if unset + """ + + self.path_to_item = path_to_item + full_msg = msg + if path_to_item: + full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) + super(ApiValueError, self).__init__(full_msg) + + +class ApiAttributeError(OpenApiException, AttributeError): + def __init__(self, msg, path_to_item=None): + """ + Raised when an attribute reference or assignment fails. + + Args: + msg (str): the exception message + + Keyword Args: + path_to_item (None/list) the path to the exception in the + received_data dict + """ + self.path_to_item = path_to_item + full_msg = msg + if path_to_item: + full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) + super(ApiAttributeError, self).__init__(full_msg) + + +class ApiKeyError(OpenApiException, KeyError): + def __init__(self, msg, path_to_item=None): + """ + Args: + msg (str): the exception message + + Keyword Args: + path_to_item (None/list) the path to the exception in the + received_data dict + """ + self.path_to_item = path_to_item + full_msg = msg + if path_to_item: + full_msg = "{0} at {1}".format(msg, render_path(path_to_item)) + super(ApiKeyError, self).__init__(full_msg) + + +class ApiException(OpenApiException): + + def __init__(self, status=None, reason=None, api_response: 'unit_test_api.api_client.ApiResponse' = None): + if api_response: + self.status = api_response.response.status + self.reason = api_response.response.reason + self.body = api_response.response.data + self.headers = api_response.response.getheaders() + else: + self.status = status + self.reason = reason + self.body = None + self.headers = None + + def __str__(self): + """Custom error messages for exception""" + error_message = "({0})\n"\ + "Reason: {1}\n".format(self.status, self.reason) + if self.headers: + error_message += "HTTP response headers: {0}\n".format( + self.headers) + + if self.body: + error_message += "HTTP response body: {0}\n".format(self.body) + + return error_message + + +def render_path(path_to_item): + """Returns a string representation of a path""" + result = "" + for pth in path_to_item: + if isinstance(pth, int): + result += "[{0}]".format(pth) + else: + result += "['{0}']".format(pth) + return result diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/model/__init__.py b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/model/__init__.py new file mode 100644 index 000000000000..0056d7a31ebe --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/model/__init__.py @@ -0,0 +1,5 @@ +# we can not import model classes here because that would create a circular +# reference which would not work in python2 +# do not import all models into this module because that uses a lot of memory and stack frames +# if you need the ability to import all models from one package, import them with +# from unit_test_api.models import ModelA, ModelB diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/model/array_type_matches_arrays.py b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/model/array_type_matches_arrays.py new file mode 100644 index 000000000000..583f9843b64c --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/model/array_type_matches_arrays.py @@ -0,0 +1,77 @@ +# coding: utf-8 + +""" + openapi 3.0.3 sample spec + + sample spec for testing openapi functionality, built from json schema tests for draft6 # noqa: E501 + + The version of the OpenAPI document: 0.0.1 + Generated by: https://openapi-generator.tech +""" + +import re # noqa: F401 +import sys # noqa: F401 +import typing # noqa: F401 +import functools # noqa: F401 + +from frozendict import frozendict # noqa: F401 + +import decimal # noqa: F401 +from datetime import date, datetime # noqa: F401 +from frozendict import frozendict # noqa: F401 + +from unit_test_api.schemas import ( # noqa: F401 + AnyTypeSchema, + ComposedSchema, + DictSchema, + ListSchema, + StrSchema, + IntSchema, + Int32Schema, + Int64Schema, + Float32Schema, + Float64Schema, + NumberSchema, + UUIDSchema, + DateSchema, + DateTimeSchema, + DecimalSchema, + BoolSchema, + BinarySchema, + NoneSchema, + none_type, + Configuration, + Unset, + unset, + ComposedBase, + ListBase, + DictBase, + NoneBase, + StrBase, + IntBase, + Int32Base, + Int64Base, + Float32Base, + Float64Base, + NumberBase, + UUIDBase, + DateBase, + DateTimeBase, + BoolBase, + BinaryBase, + Schema, + _SchemaValidator, + _SchemaTypeChecker, + _SchemaEnumMaker +) + + +class ArrayTypeMatchesArrays( + ListSchema +): + """NOTE: This class is auto generated by OpenAPI Generator. + Ref: https://openapi-generator.tech + + Do not edit the class manually. + """ + _items = AnyTypeSchema diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/model/boolean_type_matches_booleans.py b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/model/boolean_type_matches_booleans.py new file mode 100644 index 000000000000..93cd0a0505f2 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/model/boolean_type_matches_booleans.py @@ -0,0 +1,67 @@ +# coding: utf-8 + +""" + openapi 3.0.3 sample spec + + sample spec for testing openapi functionality, built from json schema tests for draft6 # noqa: E501 + + The version of the OpenAPI document: 0.0.1 + Generated by: https://openapi-generator.tech +""" + +import re # noqa: F401 +import sys # noqa: F401 +import typing # noqa: F401 +import functools # noqa: F401 + +from frozendict import frozendict # noqa: F401 + +import decimal # noqa: F401 +from datetime import date, datetime # noqa: F401 +from frozendict import frozendict # noqa: F401 + +from unit_test_api.schemas import ( # noqa: F401 + AnyTypeSchema, + ComposedSchema, + DictSchema, + ListSchema, + StrSchema, + IntSchema, + Int32Schema, + Int64Schema, + Float32Schema, + Float64Schema, + NumberSchema, + UUIDSchema, + DateSchema, + DateTimeSchema, + DecimalSchema, + BoolSchema, + BinarySchema, + NoneSchema, + none_type, + Configuration, + Unset, + unset, + ComposedBase, + ListBase, + DictBase, + NoneBase, + StrBase, + IntBase, + Int32Base, + Int64Base, + Float32Base, + Float64Base, + NumberBase, + UUIDBase, + DateBase, + DateTimeBase, + BoolBase, + BinaryBase, + Schema, + _SchemaValidator, + _SchemaTypeChecker, + _SchemaEnumMaker +) +BooleanTypeMatchesBooleans = BoolSchema diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/model/integer_type_matches_integers.py b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/model/integer_type_matches_integers.py new file mode 100644 index 000000000000..472bd37aa71a --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/model/integer_type_matches_integers.py @@ -0,0 +1,67 @@ +# coding: utf-8 + +""" + openapi 3.0.3 sample spec + + sample spec for testing openapi functionality, built from json schema tests for draft6 # noqa: E501 + + The version of the OpenAPI document: 0.0.1 + Generated by: https://openapi-generator.tech +""" + +import re # noqa: F401 +import sys # noqa: F401 +import typing # noqa: F401 +import functools # noqa: F401 + +from frozendict import frozendict # noqa: F401 + +import decimal # noqa: F401 +from datetime import date, datetime # noqa: F401 +from frozendict import frozendict # noqa: F401 + +from unit_test_api.schemas import ( # noqa: F401 + AnyTypeSchema, + ComposedSchema, + DictSchema, + ListSchema, + StrSchema, + IntSchema, + Int32Schema, + Int64Schema, + Float32Schema, + Float64Schema, + NumberSchema, + UUIDSchema, + DateSchema, + DateTimeSchema, + DecimalSchema, + BoolSchema, + BinarySchema, + NoneSchema, + none_type, + Configuration, + Unset, + unset, + ComposedBase, + ListBase, + DictBase, + NoneBase, + StrBase, + IntBase, + Int32Base, + Int64Base, + Float32Base, + Float64Base, + NumberBase, + UUIDBase, + DateBase, + DateTimeBase, + BoolBase, + BinaryBase, + Schema, + _SchemaValidator, + _SchemaTypeChecker, + _SchemaEnumMaker +) +IntegerTypeMatchesIntegers = IntSchema diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/model/null_type_matches_only_the_null_object.py b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/model/null_type_matches_only_the_null_object.py new file mode 100644 index 000000000000..3ed5b3d6b3b1 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/model/null_type_matches_only_the_null_object.py @@ -0,0 +1,67 @@ +# coding: utf-8 + +""" + openapi 3.0.3 sample spec + + sample spec for testing openapi functionality, built from json schema tests for draft6 # noqa: E501 + + The version of the OpenAPI document: 0.0.1 + Generated by: https://openapi-generator.tech +""" + +import re # noqa: F401 +import sys # noqa: F401 +import typing # noqa: F401 +import functools # noqa: F401 + +from frozendict import frozendict # noqa: F401 + +import decimal # noqa: F401 +from datetime import date, datetime # noqa: F401 +from frozendict import frozendict # noqa: F401 + +from unit_test_api.schemas import ( # noqa: F401 + AnyTypeSchema, + ComposedSchema, + DictSchema, + ListSchema, + StrSchema, + IntSchema, + Int32Schema, + Int64Schema, + Float32Schema, + Float64Schema, + NumberSchema, + UUIDSchema, + DateSchema, + DateTimeSchema, + DecimalSchema, + BoolSchema, + BinarySchema, + NoneSchema, + none_type, + Configuration, + Unset, + unset, + ComposedBase, + ListBase, + DictBase, + NoneBase, + StrBase, + IntBase, + Int32Base, + Int64Base, + Float32Base, + Float64Base, + NumberBase, + UUIDBase, + DateBase, + DateTimeBase, + BoolBase, + BinaryBase, + Schema, + _SchemaValidator, + _SchemaTypeChecker, + _SchemaEnumMaker +) +NullTypeMatchesOnlyTheNullObject = NoneSchema diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/model/number_type_matches_numbers.py b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/model/number_type_matches_numbers.py new file mode 100644 index 000000000000..3b33c38f4cd7 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/model/number_type_matches_numbers.py @@ -0,0 +1,67 @@ +# coding: utf-8 + +""" + openapi 3.0.3 sample spec + + sample spec for testing openapi functionality, built from json schema tests for draft6 # noqa: E501 + + The version of the OpenAPI document: 0.0.1 + Generated by: https://openapi-generator.tech +""" + +import re # noqa: F401 +import sys # noqa: F401 +import typing # noqa: F401 +import functools # noqa: F401 + +from frozendict import frozendict # noqa: F401 + +import decimal # noqa: F401 +from datetime import date, datetime # noqa: F401 +from frozendict import frozendict # noqa: F401 + +from unit_test_api.schemas import ( # noqa: F401 + AnyTypeSchema, + ComposedSchema, + DictSchema, + ListSchema, + StrSchema, + IntSchema, + Int32Schema, + Int64Schema, + Float32Schema, + Float64Schema, + NumberSchema, + UUIDSchema, + DateSchema, + DateTimeSchema, + DecimalSchema, + BoolSchema, + BinarySchema, + NoneSchema, + none_type, + Configuration, + Unset, + unset, + ComposedBase, + ListBase, + DictBase, + NoneBase, + StrBase, + IntBase, + Int32Base, + Int64Base, + Float32Base, + Float64Base, + NumberBase, + UUIDBase, + DateBase, + DateTimeBase, + BoolBase, + BinaryBase, + Schema, + _SchemaValidator, + _SchemaTypeChecker, + _SchemaEnumMaker +) +NumberTypeMatchesNumbers = NumberSchema diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/model/string_type_matches_strings.py b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/model/string_type_matches_strings.py new file mode 100644 index 000000000000..f1c3c9a0302c --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/model/string_type_matches_strings.py @@ -0,0 +1,67 @@ +# coding: utf-8 + +""" + openapi 3.0.3 sample spec + + sample spec for testing openapi functionality, built from json schema tests for draft6 # noqa: E501 + + The version of the OpenAPI document: 0.0.1 + Generated by: https://openapi-generator.tech +""" + +import re # noqa: F401 +import sys # noqa: F401 +import typing # noqa: F401 +import functools # noqa: F401 + +from frozendict import frozendict # noqa: F401 + +import decimal # noqa: F401 +from datetime import date, datetime # noqa: F401 +from frozendict import frozendict # noqa: F401 + +from unit_test_api.schemas import ( # noqa: F401 + AnyTypeSchema, + ComposedSchema, + DictSchema, + ListSchema, + StrSchema, + IntSchema, + Int32Schema, + Int64Schema, + Float32Schema, + Float64Schema, + NumberSchema, + UUIDSchema, + DateSchema, + DateTimeSchema, + DecimalSchema, + BoolSchema, + BinarySchema, + NoneSchema, + none_type, + Configuration, + Unset, + unset, + ComposedBase, + ListBase, + DictBase, + NoneBase, + StrBase, + IntBase, + Int32Base, + Int64Base, + Float32Base, + Float64Base, + NumberBase, + UUIDBase, + DateBase, + DateTimeBase, + BoolBase, + BinaryBase, + Schema, + _SchemaValidator, + _SchemaTypeChecker, + _SchemaEnumMaker +) +StringTypeMatchesStrings = StrSchema diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/models/__init__.py b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/models/__init__.py new file mode 100644 index 000000000000..b9d4ccb2508f --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/models/__init__.py @@ -0,0 +1,19 @@ +# coding: utf-8 + +# flake8: noqa + +# import all models into this package +# if you have many models here with many references from one model to another this may +# raise a RecursionError +# to avoid this, import only the models that you directly need like: +# from from unit_test_api.model.pet import Pet +# or import this package, but before doing it, use: +# import sys +# sys.setrecursionlimit(n) + +from unit_test_api.model.array_type_matches_arrays import ArrayTypeMatchesArrays +from unit_test_api.model.boolean_type_matches_booleans import BooleanTypeMatchesBooleans +from unit_test_api.model.integer_type_matches_integers import IntegerTypeMatchesIntegers +from unit_test_api.model.null_type_matches_only_the_null_object import NullTypeMatchesOnlyTheNullObject +from unit_test_api.model.number_type_matches_numbers import NumberTypeMatchesNumbers +from unit_test_api.model.string_type_matches_strings import StringTypeMatchesStrings diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/rest.py b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/rest.py new file mode 100644 index 000000000000..75a309eed908 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/rest.py @@ -0,0 +1,253 @@ +# coding: utf-8 + +""" + openapi 3.0.3 sample spec + + sample spec for testing openapi functionality, built from json schema tests for draft6 # noqa: E501 + + The version of the OpenAPI document: 0.0.1 + Generated by: https://openapi-generator.tech +""" + +import logging +import ssl +from urllib.parse import urlencode +import typing + +import certifi +import urllib3 +from urllib3._collections import HTTPHeaderDict + +from unit_test_api.exceptions import ApiException, ApiValueError + + +logger = logging.getLogger(__name__) + + +class RESTClientObject(object): + + def __init__(self, configuration, pools_size=4, maxsize=None): + # urllib3.PoolManager will pass all kw parameters to connectionpool + # https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/poolmanager.py#L75 # noqa: E501 + # https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/connectionpool.py#L680 # noqa: E501 + # maxsize is the number of requests to host that are allowed in parallel # noqa: E501 + # Custom SSL certificates and client certificates: http://urllib3.readthedocs.io/en/latest/advanced-usage.html # noqa: E501 + + # cert_reqs + if configuration.verify_ssl: + cert_reqs = ssl.CERT_REQUIRED + else: + cert_reqs = ssl.CERT_NONE + + # ca_certs + if configuration.ssl_ca_cert: + ca_certs = configuration.ssl_ca_cert + else: + # if not set certificate file, use Mozilla's root certificates. + ca_certs = certifi.where() + + addition_pool_args = {} + if configuration.assert_hostname is not None: + addition_pool_args['assert_hostname'] = configuration.assert_hostname # noqa: E501 + + if configuration.retries is not None: + addition_pool_args['retries'] = configuration.retries + + if configuration.socket_options is not None: + addition_pool_args['socket_options'] = configuration.socket_options + + if maxsize is None: + if configuration.connection_pool_maxsize is not None: + maxsize = configuration.connection_pool_maxsize + else: + maxsize = 4 + + # https pool manager + if configuration.proxy: + self.pool_manager = urllib3.ProxyManager( + num_pools=pools_size, + maxsize=maxsize, + cert_reqs=cert_reqs, + ca_certs=ca_certs, + cert_file=configuration.cert_file, + key_file=configuration.key_file, + proxy_url=configuration.proxy, + proxy_headers=configuration.proxy_headers, + **addition_pool_args + ) + else: + self.pool_manager = urllib3.PoolManager( + num_pools=pools_size, + maxsize=maxsize, + cert_reqs=cert_reqs, + ca_certs=ca_certs, + cert_file=configuration.cert_file, + key_file=configuration.key_file, + **addition_pool_args + ) + + def request( + self, + method: str, + url: str, + headers: typing.Optional[HTTPHeaderDict] = None, + fields: typing.Optional[typing.Tuple[typing.Tuple[str, typing.Any], ...]] = None, + body: typing.Optional[typing.Union[str, bytes]] = None, + stream: bool = False, + timeout: typing.Optional[typing.Union[int, typing.Tuple]] = None, + ) -> urllib3.HTTPResponse: + """Perform requests. + + :param method: http request method + :param url: http request url + :param headers: http request headers + :param body: request body, for other types + :param fields: request parameters for + `application/x-www-form-urlencoded` + or `multipart/form-data` + :param stream: if True, the urllib3.HTTPResponse object will + be returned without reading/decoding response + data. Default is False. + :param timeout: timeout setting for this request. If one + number provided, it will be total request + timeout. It can also be a pair (tuple) of + (connection, read) timeouts. + """ + method = method.upper() + assert method in ['GET', 'HEAD', 'DELETE', 'POST', 'PUT', + 'PATCH', 'OPTIONS'] + + if fields and body: + raise ApiValueError( + "body parameter cannot be used with fields parameter." + ) + + fields = fields or {} + headers = headers or {} + + if timeout: + if isinstance(timeout, (int, float)): # noqa: E501,F821 + timeout = urllib3.Timeout(total=timeout) + elif (isinstance(timeout, tuple) and + len(timeout) == 2): + timeout = urllib3.Timeout(connect=timeout[0], read=timeout[1]) + + try: + # For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE` + if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']: + if 'Content-Type' not in headers and body is None: + r = self.pool_manager.request( + method, + url, + preload_content=not stream, + timeout=timeout, + headers=headers + ) + elif headers['Content-Type'] == 'application/x-www-form-urlencoded': # noqa: E501 + r = self.pool_manager.request( + method, url, + fields=fields, + encode_multipart=False, + preload_content=not stream, + timeout=timeout, + headers=headers) + elif headers['Content-Type'] == 'multipart/form-data': + # must del headers['Content-Type'], or the correct + # Content-Type which generated by urllib3 will be + # overwritten. + del headers['Content-Type'] + r = self.pool_manager.request( + method, url, + fields=fields, + encode_multipart=True, + preload_content=not stream, + timeout=timeout, + headers=headers) + # Pass a `string` parameter directly in the body to support + # other content types than Json when `body` argument is + # provided in serialized form + elif isinstance(body, str) or isinstance(body, bytes): + request_body = body + r = self.pool_manager.request( + method, url, + body=request_body, + preload_content=not stream, + timeout=timeout, + headers=headers) + else: + # Cannot generate the request from given parameters + msg = """Cannot prepare a request message for provided + arguments. Please check that your arguments match + declared content type.""" + raise ApiException(status=0, reason=msg) + # For `GET`, `HEAD` + else: + r = self.pool_manager.request(method, url, + preload_content=not stream, + timeout=timeout, + headers=headers) + except urllib3.exceptions.SSLError as e: + msg = "{0}\n{1}".format(type(e).__name__, str(e)) + raise ApiException(status=0, reason=msg) + + if not stream: + # log response body + logger.debug("response body: %s", r.data) + + return r + + def GET(self, url, headers=None, stream=False, + timeout=None, fields=None) -> urllib3.HTTPResponse: + return self.request("GET", url, + headers=headers, + stream=stream, + timeout=timeout, + fields=fields) + + def HEAD(self, url, headers=None, stream=False, + timeout=None, fields=None) -> urllib3.HTTPResponse: + return self.request("HEAD", url, + headers=headers, + stream=stream, + timeout=timeout, + fields=fields) + + def OPTIONS(self, url, headers=None, + body=None, stream=False, timeout=None, fields=None) -> urllib3.HTTPResponse: + return self.request("OPTIONS", url, + headers=headers, + stream=stream, + timeout=timeout, + body=body, fields=fields) + + def DELETE(self, url, headers=None, body=None, + stream=False, timeout=None, fields=None) -> urllib3.HTTPResponse: + return self.request("DELETE", url, + headers=headers, + stream=stream, + timeout=timeout, + body=body, fields=fields) + + def POST(self, url, headers=None, + body=None, stream=False, timeout=None, fields=None) -> urllib3.HTTPResponse: + return self.request("POST", url, + headers=headers, + stream=stream, + timeout=timeout, + body=body, fields=fields) + + def PUT(self, url, headers=None, + body=None, stream=False, timeout=None, fields=None) -> urllib3.HTTPResponse: + return self.request("PUT", url, + headers=headers, + stream=stream, + timeout=timeout, + body=body, fields=fields) + + def PATCH(self, url, headers=None, + body=None, stream=False, timeout=None, fields=None) -> urllib3.HTTPResponse: + return self.request("PATCH", url, + headers=headers, + stream=stream, + timeout=timeout, + body=body, fields=fields) diff --git a/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/schemas.py b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/schemas.py new file mode 100644 index 000000000000..0b78a94e7c44 --- /dev/null +++ b/samples/openapi3/client/3_0_3_unit_test/python-experimental/unit_test_api/schemas.py @@ -0,0 +1,2201 @@ +# coding: utf-8 + +""" + openapi 3.0.3 sample spec + + sample spec for testing openapi functionality, built from json schema tests for draft6 # noqa: E501 + + The version of the OpenAPI document: 0.0.1 + Generated by: https://openapi-generator.tech +""" + +from collections import defaultdict +from datetime import date, datetime, timedelta # noqa: F401 +import functools +import decimal +import io +import os +import re +import tempfile +import typing +import uuid + +from dateutil.parser.isoparser import isoparser, _takes_ascii +from frozendict import frozendict + +from unit_test_api.exceptions import ( + ApiTypeError, + ApiValueError, +) +from unit_test_api.configuration import ( + Configuration, +) + + +class Unset(object): + """ + An instance of this class is set as the default value for object type(dict) properties that are optional + When a property has an unset value, that property will not be assigned in the dict + """ + pass + +unset = Unset() + +none_type = type(None) +file_type = io.IOBase + + +class FileIO(io.FileIO): + """ + A class for storing files + Note: this class is not immutable + """ + + def __new__(cls, arg: typing.Union[io.FileIO, io.BufferedReader]): + if isinstance(arg, (io.FileIO, io.BufferedReader)): + arg.close() + inst = super(FileIO, cls).__new__(cls, arg.name) + super(FileIO, inst).__init__(arg.name) + return inst + raise ApiValueError('FileIO must be passed arg which contains the open file') + + +def update(d: dict, u: dict): + """ + Adds u to d + Where each dict is defaultdict(set) + """ + if not u: + return d + for k, v in u.items(): + if not v: + continue + if k not in d: + d[k] = v + else: + d[k] = d[k] | v + + +class ValidationMetadata(frozendict): + """ + A class storing metadata that is needed to validate OpenApi Schema payloads + """ + def __new__( + cls, + path_to_item: typing.Tuple[typing.Union[str, int], ...] = tuple(['args[0]']), + from_server: bool = False, + configuration: typing.Optional[Configuration] = None, + base_classes: typing.FrozenSet[typing.Type] = frozenset(), + ): + """ + Args: + path_to_item: the path to the current data being instantiated. + For {'a': [1]} if the code is handling, 1, then the path is ('args[0]', 'a', 0) + from_server: whether or not this data came form the server + True when receiving server data + False when instantiating model with client side data not form the server + configuration: the Configuration instance to use + This is needed because in Configuration: + - one can disable validation checking + base_classes: when deserializing data that matches multiple schemas, this is used to store + the schemas that have been traversed. This is used to stop processing when a cycle is seen. + """ + return super().__new__( + cls, + path_to_item=path_to_item, + from_server=from_server, + configuration=configuration, + base_classes=base_classes, + ) + + @property + def path_to_item(self) -> typing.Tuple[typing.Union[str, int], ...]: + return self.get('path_to_item') + + @property + def from_server(self) -> bool: + return self.get('from_server') + + @property + def configuration(self) -> typing.Optional[Configuration]: + return self.get('configuration') + + @property + def base_classes(self) -> typing.FrozenSet[typing.Type]: + return self.get('base_classes') + + +class ValidatorBase: + @staticmethod + def __is_json_validation_enabled(schema_keyword, configuration=None): + """Returns true if JSON schema validation is enabled for the specified + validation keyword. This can be used to skip JSON schema structural validation + as requested in the configuration. + + Args: + schema_keyword (string): the name of a JSON schema validation keyword. + configuration (Configuration): the configuration class. + """ + + return (configuration is None or + not hasattr(configuration, '_disabled_client_side_validations') or + schema_keyword not in configuration._disabled_client_side_validations) + + @staticmethod + def __raise_validation_error_message(value, constraint_msg, constraint_value, path_to_item, additional_txt=""): + raise ApiValueError( + "Invalid value `{value}`, {constraint_msg} `{constraint_value}`{additional_txt} at {path_to_item}".format( + value=value, + constraint_msg=constraint_msg, + constraint_value=constraint_value, + additional_txt=additional_txt, + path_to_item=path_to_item, + ) + ) + + @classmethod + def __check_str_validations(cls, + validations, input_values, + validation_metadata: ValidationMetadata): + + if (cls.__is_json_validation_enabled('maxLength', validation_metadata.configuration) and + 'max_length' in validations and + len(input_values) > validations['max_length']): + cls.__raise_validation_error_message( + value=input_values, + constraint_msg="length must be less than or equal to", + constraint_value=validations['max_length'], + path_to_item=validation_metadata.path_to_item + ) + + if (cls.__is_json_validation_enabled('minLength', validation_metadata.configuration) and + 'min_length' in validations and + len(input_values) < validations['min_length']): + cls.__raise_validation_error_message( + value=input_values, + constraint_msg="length must be greater than or equal to", + constraint_value=validations['min_length'], + path_to_item=validation_metadata.path_to_item + ) + + checked_value = input_values + if (cls.__is_json_validation_enabled('pattern', validation_metadata.configuration) and + 'regex' in validations): + for regex_dict in validations['regex']: + flags = regex_dict.get('flags', 0) + if not re.search(regex_dict['pattern'], checked_value, flags=flags): + if flags != 0: + # Don't print the regex flags if the flags are not + # specified in the OAS document. + cls.__raise_validation_error_message( + value=input_values, + constraint_msg="must match regular expression", + constraint_value=regex_dict['pattern'], + path_to_item=validation_metadata.path_to_item, + additional_txt=" with flags=`{}`".format(flags) + ) + cls.__raise_validation_error_message( + value=input_values, + constraint_msg="must match regular expression", + constraint_value=regex_dict['pattern'], + path_to_item=validation_metadata.path_to_item + ) + + @classmethod + def __check_tuple_validations( + cls, validations, input_values, + validation_metadata: ValidationMetadata): + + if (cls.__is_json_validation_enabled('maxItems', validation_metadata.configuration) and + 'max_items' in validations and + len(input_values) > validations['max_items']): + cls.__raise_validation_error_message( + value=input_values, + constraint_msg="number of items must be less than or equal to", + constraint_value=validations['max_items'], + path_to_item=validation_metadata.path_to_item + ) + + if (cls.__is_json_validation_enabled('minItems', validation_metadata.configuration) and + 'min_items' in validations and + len(input_values) < validations['min_items']): + cls.__raise_validation_error_message( + value=input_values, + constraint_msg="number of items must be greater than or equal to", + constraint_value=validations['min_items'], + path_to_item=validation_metadata.path_to_item + ) + + if (cls.__is_json_validation_enabled('uniqueItems', validation_metadata.configuration) and + 'unique_items' in validations and validations['unique_items'] and input_values): + unique_items = [] + for item in input_values: + if item not in unique_items: + unique_items.append(item) + if len(input_values) > len(unique_items): + cls.__raise_validation_error_message( + value=input_values, + constraint_msg="duplicate items were found, and the tuple must not contain duplicates because", + constraint_value='unique_items==True', + path_to_item=validation_metadata.path_to_item + ) + + @classmethod + def __check_dict_validations( + cls, validations, input_values, + validation_metadata: ValidationMetadata): + + if (cls.__is_json_validation_enabled('maxProperties', validation_metadata.configuration) and + 'max_properties' in validations and + len(input_values) > validations['max_properties']): + cls.__raise_validation_error_message( + value=input_values, + constraint_msg="number of properties must be less than or equal to", + constraint_value=validations['max_properties'], + path_to_item=validation_metadata.path_to_item + ) + + if (cls.__is_json_validation_enabled('minProperties', validation_metadata.configuration) and + 'min_properties' in validations and + len(input_values) < validations['min_properties']): + cls.__raise_validation_error_message( + value=input_values, + constraint_msg="number of properties must be greater than or equal to", + constraint_value=validations['min_properties'], + path_to_item=validation_metadata.path_to_item + ) + + @classmethod + def __check_numeric_validations( + cls, validations, input_values, + validation_metadata: ValidationMetadata): + + if cls.__is_json_validation_enabled('multipleOf', + validation_metadata.configuration) and 'multiple_of' in validations: + multiple_of_values = validations['multiple_of'] + for multiple_of_value in multiple_of_values: + if (isinstance(input_values, decimal.Decimal) and + not (float(input_values) / multiple_of_value).is_integer() + ): + # Note 'multipleOf' will be as good as the floating point arithmetic. + cls.__raise_validation_error_message( + value=input_values, + constraint_msg="value must be a multiple of", + constraint_value=multiple_of_value, + path_to_item=validation_metadata.path_to_item + ) + + checking_max_or_min_values = {'exclusive_maximum', 'inclusive_maximum', 'exclusive_minimum', + 'inclusive_minimum'}.isdisjoint(validations) is False + if not checking_max_or_min_values: + return + max_val = input_values + min_val = input_values + + if (cls.__is_json_validation_enabled('exclusiveMaximum', validation_metadata.configuration) and + 'exclusive_maximum' in validations and + max_val >= validations['exclusive_maximum']): + cls.__raise_validation_error_message( + value=input_values, + constraint_msg="must be a value less than", + constraint_value=validations['exclusive_maximum'], + path_to_item=validation_metadata.path_to_item + ) + + if (cls.__is_json_validation_enabled('maximum', validation_metadata.configuration) and + 'inclusive_maximum' in validations and + max_val > validations['inclusive_maximum']): + cls.__raise_validation_error_message( + value=input_values, + constraint_msg="must be a value less than or equal to", + constraint_value=validations['inclusive_maximum'], + path_to_item=validation_metadata.path_to_item + ) + + if (cls.__is_json_validation_enabled('exclusiveMinimum', validation_metadata.configuration) and + 'exclusive_minimum' in validations and + min_val <= validations['exclusive_minimum']): + cls.__raise_validation_error_message( + value=input_values, + constraint_msg="must be a value greater than", + constraint_value=validations['exclusive_maximum'], + path_to_item=validation_metadata.path_to_item + ) + + if (cls.__is_json_validation_enabled('minimum', validation_metadata.configuration) and + 'inclusive_minimum' in validations and + min_val < validations['inclusive_minimum']): + cls.__raise_validation_error_message( + value=input_values, + constraint_msg="must be a value greater than or equal to", + constraint_value=validations['inclusive_minimum'], + path_to_item=validation_metadata.path_to_item + ) + + @classmethod + def _check_validations_for_types( + cls, + validations, + input_values, + validation_metadata: ValidationMetadata + ): + if isinstance(input_values, str): + cls.__check_str_validations(validations, input_values, validation_metadata) + elif isinstance(input_values, tuple): + cls.__check_tuple_validations(validations, input_values, validation_metadata) + elif isinstance(input_values, frozendict): + cls.__check_dict_validations(validations, input_values, validation_metadata) + elif isinstance(input_values, decimal.Decimal): + cls.__check_numeric_validations(validations, input_values, validation_metadata) + try: + return super()._validate_validations_pass(input_values, validation_metadata) + except AttributeError: + return True + + +class Validator(typing.Protocol): + def _validate_validations_pass( + cls, + input_values, + validation_metadata: ValidationMetadata + ): + pass + + +def _SchemaValidator(**validations: typing.Union[str, bool, None, int, float, list[dict[str, typing.Union[str, int, float]]]]) -> Validator: + class SchemaValidator(ValidatorBase): + @classmethod + def _validate_validations_pass( + cls, + input_values, + validation_metadata: ValidationMetadata + ): + cls._check_validations_for_types(validations, input_values, validation_metadata) + try: + return super()._validate_validations_pass(input_values, validation_metadata) + except AttributeError: + return True + + return SchemaValidator + + +class TypeChecker(typing.Protocol): + @classmethod + def _validate_type( + cls, arg_simple_class: type + ) -> typing.Tuple[type]: + pass + + +def _SchemaTypeChecker(union_type_cls: typing.Union[typing.Any]) -> TypeChecker: + if typing.get_origin(union_type_cls) is typing.Union: + union_classes = typing.get_args(union_type_cls) + else: + # note: when a union of a single class is passed in, the union disappears + union_classes = tuple([union_type_cls]) + """ + I want the type hint... union_type_cls + and to use it as a base class but when I do, I get + TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases + """ + class SchemaTypeChecker: + @classmethod + def _validate_type(cls, arg_simple_class: type): + if arg_simple_class not in union_classes: + return union_classes + try: + return super()._validate_type(arg_simple_class) + except AttributeError: + return tuple() + + return SchemaTypeChecker + + +class EnumMakerBase: + @classmethod + @property + def _enum_by_value( + cls + ) -> type: + enum_classes = {} + if not hasattr(cls, "_enum_value_to_name"): + return enum_classes + for enum_value, enum_name in cls._enum_value_to_name.items(): + base_class = type(enum_value) + if base_class is none_type: + enum_classes[enum_value] = get_new_class( + "Dynamic" + cls.__name__, (cls, NoneClass)) + log_cache_usage(get_new_class) + elif base_class is bool: + enum_classes[enum_value] = get_new_class( + "Dynamic" + cls.__name__, (cls, BoolClass)) + log_cache_usage(get_new_class) + else: + enum_classes[enum_value] = get_new_class( + "Dynamic" + cls.__name__, (cls, Singleton, base_class)) + log_cache_usage(get_new_class) + return enum_classes + + +class EnumMakerInterface(typing.Protocol): + @classmethod + @property + def _enum_value_to_name( + cls + ) -> typing.Dict[typing.Union[str, decimal.Decimal, bool, none_type], str]: + pass + + @classmethod + @property + def _enum_by_value( + cls + ) -> type: + pass + + +def _SchemaEnumMaker(enum_value_to_name: typing.Dict[typing.Union[str, decimal.Decimal, bool, none_type], str]) -> EnumMakerInterface: + class SchemaEnumMaker(EnumMakerBase): + @classmethod + @property + def _enum_value_to_name( + cls + ) -> typing.Dict[typing.Union[str, decimal.Decimal, bool, none_type], str]: + pass + try: + super_enum_value_to_name = super()._enum_value_to_name + except AttributeError: + return enum_value_to_name + intersection = dict(enum_value_to_name.items() & super_enum_value_to_name.items()) + return intersection + + return SchemaEnumMaker + + +class Singleton: + """ + Enums and singletons are the same + The same instance is returned for a given key of (cls, arg) + """ + _instances = {} + + def __new__(cls, arg: typing.Any, **kwargs): + key = (cls, arg) + if key not in cls._instances: + if arg in {None, True, False}: + inst = super().__new__(cls) + # inst._value = arg + cls._instances[key] = inst + else: + cls._instances[key] = super().__new__(cls, arg) + return cls._instances[key] + + def __repr__(self): + if isinstance(self, NoneClass): + return f'<{self.__class__.__name__}: None>' + elif isinstance(self, BoolClass): + if (self.__class__, True) in self._instances: + return f'<{self.__class__.__name__}: True>' + return f'<{self.__class__.__name__}: False>' + return f'<{self.__class__.__name__}: {super().__repr__()}>' + + +class NoneClass(Singleton): + @classmethod + @property + def NONE(cls): + return cls(None) + + def __bool__(self) -> bool: + return False + + +class BoolClass(Singleton): + @classmethod + @property + def TRUE(cls): + return cls(True) + + @classmethod + @property + def FALSE(cls): + return cls(False) + + @functools.cache + def __bool__(self) -> bool: + for key, instance in self._instances.items(): + if self is instance: + return key[1] + raise ValueError('Unable to find the boolean value of this instance') + + +class BoolBase: + def is_true(self) -> bool: + """ + A replacement for x is True + True if the instance is a BoolClass True Singleton + """ + if not issubclass(self.__class__, BoolClass): + return False + return bool(self) + + def is_false(self) -> bool: + """ + A replacement for x is False + True if the instance is a BoolClass False Singleton + """ + if not issubclass(self.__class__, BoolClass): + return False + return bool(self) is False + + +class NoneBase: + def is_none(self) -> bool: + """ + A replacement for x is None + True if the instance is a NoneClass None Singleton + """ + if issubclass(self.__class__, NoneClass): + return True + return False + + +class StrBase: + @property + def as_str(self) -> str: + return self + + @property + def as_date(self) -> date: + raise Exception('not implemented') + + @property + def as_datetime(self) -> datetime: + raise Exception('not implemented') + + @property + def as_decimal(self) -> decimal.Decimal: + raise Exception('not implemented') + + @property + def as_uuid(self) -> uuid.UUID: + raise Exception('not implemented') + + +class UUIDBase(StrBase): + @property + @functools.cache + def as_uuid(self) -> uuid.UUID: + return uuid.UUID(self) + + @classmethod + def _validate_format(cls, arg: typing.Optional[str], validation_metadata: ValidationMetadata): + if isinstance(arg, str): + try: + uuid.UUID(arg) + return True + except ValueError: + raise ApiValueError( + "Invalid value '{}' for type UUID at {}".format(arg, validation_metadata.path_to_item) + ) + + @classmethod + def _validate( + cls, + arg, + validation_metadata: typing.Optional[ValidationMetadata] = None, + ): + """ + UUIDBase _validate + """ + cls._validate_format(arg, validation_metadata=validation_metadata) + return super()._validate(arg, validation_metadata=validation_metadata) + + +class CustomIsoparser(isoparser): + + @_takes_ascii + def parse_isodatetime(self, dt_str): + components, pos = self._parse_isodate(dt_str) + if len(dt_str) > pos: + if self._sep is None or dt_str[pos:pos + 1] == self._sep: + components += self._parse_isotime(dt_str[pos + 1:]) + else: + raise ValueError('String contains unknown ISO components') + + if len(components) > 3 and components[3] == 24: + components[3] = 0 + return datetime(*components) + timedelta(days=1) + + if len(components) <= 3: + raise ValueError('Value is not a datetime') + + return datetime(*components) + + @_takes_ascii + def parse_isodate(self, datestr): + components, pos = self._parse_isodate(datestr) + + if len(datestr) > pos: + raise ValueError('String contains invalid time components') + + if len(components) > 3: + raise ValueError('String contains invalid time components') + + return date(*components) + + +DEFAULT_ISOPARSER = CustomIsoparser() + + +class DateBase(StrBase): + @property + @functools.cache + def as_date(self) -> date: + return DEFAULT_ISOPARSER.parse_isodate(self) + + @classmethod + def _validate_format(cls, arg: typing.Optional[str], validation_metadata: ValidationMetadata): + if isinstance(arg, str): + try: + DEFAULT_ISOPARSER.parse_isodate(arg) + return True + except ValueError: + raise ApiValueError( + "Value does not conform to the required ISO-8601 date format. " + "Invalid value '{}' for type date at {}".format(arg, validation_metadata.path_to_item) + ) + + @classmethod + def _validate( + cls, + arg, + validation_metadata: typing.Optional[ValidationMetadata] = None, + ): + """ + DateBase _validate + """ + cls._validate_format(arg, validation_metadata=validation_metadata) + return super()._validate(arg, validation_metadata=validation_metadata) + + +class DateTimeBase: + @property + @functools.cache + def as_datetime(self) -> datetime: + return DEFAULT_ISOPARSER.parse_isodatetime(self) + + @classmethod + def _validate_format(cls, arg: typing.Optional[str], validation_metadata: ValidationMetadata): + if isinstance(arg, str): + try: + DEFAULT_ISOPARSER.parse_isodatetime(arg) + return True + except ValueError: + raise ApiValueError( + "Value does not conform to the required ISO-8601 datetime format. " + "Invalid value '{}' for type datetime at {}".format(arg, validation_metadata.path_to_item) + ) + + @classmethod + def _validate( + cls, + arg, + validation_metadata: ValidationMetadata, + ): + """ + DateTimeBase _validate + """ + cls._validate_format(arg, validation_metadata=validation_metadata) + return super()._validate(arg, validation_metadata=validation_metadata) + + +class DecimalBase(StrBase): + """ + A class for storing decimals that are sent over the wire as strings + These schemas must remain based on StrBase rather than NumberBase + because picking base classes must be deterministic + """ + + @property + @functools.cache + def as_decimal(self) -> decimal.Decimal: + return decimal.Decimal(self) + + @classmethod + def _validate_format(cls, arg: typing.Optional[str], validation_metadata: ValidationMetadata): + if isinstance(arg, str): + try: + decimal.Decimal(arg) + return True + except decimal.InvalidOperation: + raise ApiValueError( + "Value cannot be converted to a decimal. " + "Invalid value '{}' for type decimal at {}".format(arg, validation_metadata.path_to_item) + ) + + @classmethod + def _validate( + cls, + arg, + validation_metadata: ValidationMetadata, + ): + """ + DecimalBase _validate + """ + cls._validate_format(arg, validation_metadata=validation_metadata) + return super()._validate(arg, validation_metadata=validation_metadata) + + +class NumberBase: + @property + def as_int(self) -> int: + try: + return self._as_int + except AttributeError: + """ + Note: for some numbers like 9.0 they could be represented as an + integer but our code chooses to store them as + >>> Decimal('9.0').as_tuple() + DecimalTuple(sign=0, digits=(9, 0), exponent=-1) + so we can tell that the value came from a float and convert it back to a float + during later serialization + """ + if self.as_tuple().exponent < 0: + # this could be represented as an integer but should be represented as a float + # because that's what it was serialized from + raise ApiValueError(f'{self} is not an integer') + self._as_int = int(self) + return self._as_int + + @property + def as_float(self) -> float: + try: + return self._as_float + except AttributeError: + if self.as_tuple().exponent >= 0: + raise ApiValueError(f'{self} is not an float') + self._as_float = float(self) + return self._as_float + + +class ListBase: + @classmethod + def _validate_items(cls, list_items, validation_metadata: ValidationMetadata): + """ + Ensures that: + - values passed in for items are valid + Exceptions will be raised if: + - invalid arguments were passed in + + Args: + list_items: the input list of items + + Raises: + ApiTypeError - for missing required arguments, or for invalid properties + """ + + # if we have definitions for an items schema, use it + # otherwise accept anything + item_cls = getattr(cls, '_items', AnyTypeSchema) + path_to_schemas = {} + for i, value in enumerate(list_items): + if isinstance(value, item_cls): + continue + item_validation_metadata = ValidationMetadata( + from_server=validation_metadata.from_server, + configuration=validation_metadata.configuration, + path_to_item=validation_metadata.path_to_item+(i,) + ) + other_path_to_schemas = item_cls._validate( + value, validation_metadata=item_validation_metadata) + update(path_to_schemas, other_path_to_schemas) + return path_to_schemas + + @classmethod + def _validate( + cls, + arg, + validation_metadata: ValidationMetadata, + ): + """ + ListBase _validate + We return dynamic classes of different bases depending upon the inputs + This makes it so: + - the returned instance is always a subclass of our defining schema + - this allows us to check type based on whether an instance is a subclass of a schema + - the returned instance is a serializable type (except for None, True, and False) which are enums + + Returns: + new_cls (type): the new class + + Raises: + ApiValueError: when a string can't be converted into a date or datetime and it must be one of those classes + ApiTypeError: when the input type is not in the list of allowed spec types + """ + _path_to_schemas = super()._validate(arg, validation_metadata=validation_metadata) + if not isinstance(arg, tuple): + return _path_to_schemas + if cls in validation_metadata.base_classes: + # we have already moved through this class so stop here + return _path_to_schemas + updated_vm = ValidationMetadata( + configuration=validation_metadata.configuration, + from_server=validation_metadata.from_server, + path_to_item=validation_metadata.path_to_item, + base_classes=validation_metadata.base_classes | frozenset({cls}) + ) + other_path_to_schemas = cls._validate_items(arg, validation_metadata=updated_vm) + update(_path_to_schemas, other_path_to_schemas) + return _path_to_schemas + + @classmethod + def _get_items( + cls: 'Schema', + arg: typing.List[typing.Any], + path_to_item: typing.Tuple[typing.Union[str, int], ...], + path_to_schemas: typing.Dict[typing.Tuple[typing.Union[str, int], ...], typing.Type['Schema']] + ): + ''' + ListBase _get_items + ''' + list_items = arg + cast_items = [] + # if we have definitions for an items schema, use it + # otherwise accept anything + + cls_item_cls = getattr(cls, '_items', AnyTypeSchema) + for i, value in enumerate(list_items): + item_path_to_item = path_to_item + (i,) + item_cls = path_to_schemas.get(item_path_to_item) + if item_cls is None: + item_cls = cls_item_cls + + if isinstance(value, item_cls): + cast_items.append(value) + continue + + new_value = item_cls._get_new_instance_without_conversion( + value, + item_path_to_item, + path_to_schemas + ) + cast_items.append(new_value) + + return cast_items + + +class Discriminable: + @classmethod + def _ensure_discriminator_value_present(cls, disc_property_name: str, validation_metadata: ValidationMetadata, *args): + if not args or args and disc_property_name not in args[0]: + # The input data does not contain the discriminator property + raise ApiValueError( + "Cannot deserialize input data due to missing discriminator. " + "The discriminator property '{}' is missing at path: {}".format(disc_property_name, validation_metadata.path_to_item) + ) + + @classmethod + def _get_discriminated_class(cls, disc_property_name: str, disc_payload_value: str): + """ + Used in schemas with discriminators + """ + if not hasattr(cls, '_discriminator'): + return None + disc = cls._discriminator + if disc_property_name not in disc: + return None + discriminated_cls = disc[disc_property_name].get(disc_payload_value) + if discriminated_cls is not None: + return discriminated_cls + elif not hasattr(cls, '_composed_schemas'): + return None + # TODO stop traveling if a cycle is hit + for allof_cls in cls._composed_schemas['allOf']: + discriminated_cls = allof_cls._get_discriminated_class( + disc_property_name=disc_property_name, disc_payload_value=disc_payload_value) + if discriminated_cls is not None: + return discriminated_cls + for oneof_cls in cls._composed_schemas['oneOf']: + discriminated_cls = oneof_cls._get_discriminated_class( + disc_property_name=disc_property_name, disc_payload_value=disc_payload_value) + if discriminated_cls is not None: + return discriminated_cls + for anyof_cls in cls._composed_schemas['anyOf']: + discriminated_cls = anyof_cls._get_discriminated_class( + disc_property_name=disc_property_name, disc_payload_value=disc_payload_value) + if discriminated_cls is not None: + return discriminated_cls + return None + + +class DictBase(Discriminable): + # subclass properties + _required_property_names = set() + + @classmethod + def _validate_arg_presence(cls, arg): + """ + Ensures that: + - all required arguments are passed in + - the input variable names are valid + - present in properties or + - accepted because additionalProperties exists + Exceptions will be raised if: + - invalid arguments were passed in + - a var_name is invalid if additionProperties == None and var_name not in _properties + - required properties were not passed in + + Args: + arg: the input dict + + Raises: + ApiTypeError - for missing required arguments, or for invalid properties + """ + seen_required_properties = set() + invalid_arguments = [] + for property_name in arg: + if property_name in cls._required_property_names: + seen_required_properties.add(property_name) + elif property_name in cls._property_names: + continue + elif cls._additional_properties: + continue + else: + invalid_arguments.append(property_name) + missing_required_arguments = list(cls._required_property_names - seen_required_properties) + if missing_required_arguments: + missing_required_arguments.sort() + raise ApiTypeError( + "{} is missing {} required argument{}: {}".format( + cls.__name__, + len(missing_required_arguments), + "s" if len(missing_required_arguments) > 1 else "", + missing_required_arguments + ) + ) + if invalid_arguments: + invalid_arguments.sort() + raise ApiTypeError( + "{} was passed {} invalid argument{}: {}".format( + cls.__name__, + len(invalid_arguments), + "s" if len(invalid_arguments) > 1 else "", + invalid_arguments + ) + ) + + @classmethod + def _validate_args(cls, arg, validation_metadata: ValidationMetadata): + """ + Ensures that: + - values passed in for properties are valid + Exceptions will be raised if: + - invalid arguments were passed in + + Args: + arg: the input dict + + Raises: + ApiTypeError - for missing required arguments, or for invalid properties + """ + path_to_schemas = {} + for property_name, value in arg.items(): + if property_name in cls._required_property_names or property_name in cls._property_names: + schema = getattr(cls, property_name) + elif cls._additional_properties: + schema = cls._additional_properties + else: + raise ApiTypeError('Unable to find schema for value={} in class={} at path_to_item={}'.format( + value, cls, validation_metadata.path_to_item+(property_name,) + )) + if isinstance(value, schema): + continue + arg_validation_metadata = ValidationMetadata( + from_server=validation_metadata.from_server, + configuration=validation_metadata.configuration, + path_to_item=validation_metadata.path_to_item+(property_name,) + ) + other_path_to_schemas = schema._validate(value, validation_metadata=arg_validation_metadata) + update(path_to_schemas, other_path_to_schemas) + return path_to_schemas + + @classmethod + def _validate( + cls, + arg, + validation_metadata: ValidationMetadata, + ): + """ + DictBase _validate + We return dynamic classes of different bases depending upon the inputs + This makes it so: + - the returned instance is always a subclass of our defining schema + - this allows us to check type based on whether an instance is a subclass of a schema + - the returned instance is a serializable type (except for None, True, and False) which are enums + + Returns: + new_cls (type): the new class + + Raises: + ApiValueError: when a string can't be converted into a date or datetime and it must be one of those classes + ApiTypeError: when the input type is not in the list of allowed spec types + """ + if isinstance(arg, cls): + # an instance of the correct type was passed in + return {} + _path_to_schemas = super()._validate(arg, validation_metadata=validation_metadata) + if not isinstance(arg, frozendict): + return _path_to_schemas + cls._validate_arg_presence(arg) + other_path_to_schemas = cls._validate_args(arg, validation_metadata=validation_metadata) + update(_path_to_schemas, other_path_to_schemas) + try: + _discriminator = cls._discriminator + except AttributeError: + return _path_to_schemas + # discriminator exists + disc_prop_name = list(_discriminator.keys())[0] + cls._ensure_discriminator_value_present(disc_prop_name, validation_metadata, arg) + discriminated_cls = cls._get_discriminated_class( + disc_property_name=disc_prop_name, disc_payload_value=arg[disc_prop_name]) + if discriminated_cls is None: + raise ApiValueError( + "Invalid discriminator value was passed in to {}.{} Only the values {} are allowed at {}".format( + cls.__name__, + disc_prop_name, + list(_discriminator[disc_prop_name].keys()), + validation_metadata.path_to_item + (disc_prop_name,) + ) + ) + if discriminated_cls in validation_metadata.base_classes: + # we have already moved through this class so stop here + return _path_to_schemas + updated_vm = ValidationMetadata( + configuration=validation_metadata.configuration, + from_server=validation_metadata.from_server, + path_to_item=validation_metadata.path_to_item, + base_classes=validation_metadata.base_classes | frozenset({cls}) + ) + other_path_to_schemas = discriminated_cls._validate(arg, validation_metadata=updated_vm) + update(_path_to_schemas, other_path_to_schemas) + return _path_to_schemas + + @classmethod + @property + def _additional_properties(cls): + return AnyTypeSchema + + @classmethod + @property + @functools.cache + def _property_names(cls): + property_names = set() + for var_name, var_value in cls.__dict__.items(): + # referenced models are classmethods + is_classmethod = type(var_value) is classmethod + if is_classmethod: + property_names.add(var_name) + continue + is_class = type(var_value) is type + if not is_class: + continue + if not issubclass(var_value, Schema): + continue + if var_name == '_additional_properties': + continue + property_names.add(var_name) + property_names = list(property_names) + property_names.sort() + return tuple(property_names) + + @classmethod + def _get_properties( + cls, + arg: typing.Dict[str, typing.Any], + path_to_item: typing.Tuple[typing.Union[str, int], ...], + path_to_schemas: typing.Dict[typing.Tuple[typing.Union[str, int], ...], typing.Type['Schema']] + ): + """ + DictBase _get_properties, this is how properties are set + These values already passed validation + """ + dict_items = {} + # if we have definitions for property schemas convert values using it + # otherwise accept anything + + for property_name_js, value in arg.items(): + property_cls = getattr(cls, property_name_js, cls._additional_properties) + property_path_to_item = path_to_item + (property_name_js,) + stored_property_cls = path_to_schemas.get(property_path_to_item) + if stored_property_cls: + property_cls = stored_property_cls + + if isinstance(value, property_cls): + dict_items[property_name_js] = value + continue + + new_value = property_cls._get_new_instance_without_conversion( + value, + property_path_to_item, + path_to_schemas + ) + dict_items[property_name_js] = new_value + return dict_items + + def __setattr__(self, name, value): + if not isinstance(self, FileIO): + raise AttributeError('property setting not supported on immutable instances') + + def __getattr__(self, name): + if isinstance(self, frozendict): + # if an attribute does not exist + try: + return self[name] + except KeyError as ex: + raise AttributeError(str(ex)) + return super().__getattr__(self, name) + + def __getattribute__(self, name): + # if an attribute does exist (for example as a class property but not as an instance method) + try: + return self[name] + except (KeyError, TypeError): + return super().__getattribute__(name) + + +inheritable_primitive_types_set = {decimal.Decimal, str, tuple, frozendict, FileIO, bytes} + + +class Schema: + """ + the base class of all swagger/openapi schemas/models + + ensures that: + - payload passes required validations + - payload is of allowed types + - payload value is an allowed enum value + """ + + @staticmethod + def __get_simple_class(input_value): + """Returns an input_value's simple class that we will use for type checking + + Args: + input_value (class/class_instance): the item for which we will return + the simple class + """ + if isinstance(input_value, tuple): + return tuple + elif isinstance(input_value, frozendict): + return frozendict + elif isinstance(input_value, none_type): + return none_type + elif isinstance(input_value, bytes): + return bytes + elif isinstance(input_value, (io.FileIO, io.BufferedReader)): + return FileIO + elif isinstance(input_value, bool): + # this must be higher than the int check because + # isinstance(True, int) == True + return bool + elif isinstance(input_value, int): + return int + elif isinstance(input_value, float): + return float + elif isinstance(input_value, datetime): + # this must be higher than the date check because + # isinstance(datetime_instance, date) == True + return datetime + elif isinstance(input_value, date): + return date + elif isinstance(input_value, str): + return str + return type(input_value) + + @staticmethod + def __get_valid_classes_phrase(input_classes): + """Returns a string phrase describing what types are allowed""" + all_classes = list(input_classes) + all_classes = sorted(all_classes, key=lambda cls: cls.__name__) + all_class_names = [cls.__name__ for cls in all_classes] + if len(all_class_names) == 1: + return "is {0}".format(all_class_names[0]) + return "is one of [{0}]".format(", ".join(all_class_names)) + + @classmethod + def __type_error_message( + cls, var_value=None, var_name=None, valid_classes=None, key_type=None + ): + """ + Keyword Args: + var_value (any): the variable which has the type_error + var_name (str): the name of the variable which has the typ error + valid_classes (tuple): the accepted classes for current_item's + value + key_type (bool): False if our value is a value in a dict + True if it is a key in a dict + False if our item is an item in a tuple + """ + key_or_value = "value" + if key_type: + key_or_value = "key" + valid_classes_phrase = cls.__get_valid_classes_phrase(valid_classes) + msg = "Invalid type. Required {1} type {2} and " "passed type was {3}".format( + var_name, + key_or_value, + valid_classes_phrase, + type(var_value).__name__, + ) + return msg + + @classmethod + def __get_type_error(cls, var_value, path_to_item, valid_classes, key_type=False): + error_msg = cls.__type_error_message( + var_name=path_to_item[-1], + var_value=var_value, + valid_classes=valid_classes, + key_type=key_type, + ) + return ApiTypeError( + error_msg, + path_to_item=path_to_item, + valid_classes=valid_classes, + key_type=key_type, + ) + + @classmethod + def _class_by_base_class(cls, base_cls: type) -> type: + cls_name = "Dynamic"+cls.__name__ + if base_cls is bool: + new_cls = get_new_class(cls_name, (cls, BoolBase, BoolClass)) + elif base_cls is str: + new_cls = get_new_class(cls_name, (cls, StrBase, str)) + elif base_cls is decimal.Decimal: + new_cls = get_new_class(cls_name, (cls, NumberBase, decimal.Decimal)) + elif base_cls is tuple: + new_cls = get_new_class(cls_name, (cls, ListBase, tuple)) + elif base_cls is frozendict: + new_cls = get_new_class(cls_name, (cls, DictBase, frozendict)) + elif base_cls is none_type: + new_cls = get_new_class(cls_name, (cls, NoneBase, NoneClass)) + log_cache_usage(get_new_class) + return new_cls + + @classmethod + def _validate( + cls, + arg, + validation_metadata: ValidationMetadata, + ): + """ + Schema _validate + Runs all schema validation logic and + returns a dynamic class of different bases depending upon the input + This makes it so: + - the returned instance is always a subclass of our defining schema + - this allows us to check type based on whether an instance is a subclass of a schema + - the returned instance is a serializable type (except for None, True, and False) which are enums + + Use cases: + 1. inheritable type: string/decimal.Decimal/frozendict/tuple + 2. enum value cases: 'hi', 1 -> no base_class set because the enum includes the base class + 3. uninheritable type: True/False/None -> no base_class because the base class is not inheritable + _enum_by_value will handle this use case + + Required Steps: + 1. verify type of input is valid vs the allowed _types + 2. check validations that are applicable for this type of input + 3. if enums exist, check that the value exists in the enum + + Returns: + path_to_schemas: a map of path to schemas + + Raises: + ApiValueError: when a string can't be converted into a date or datetime and it must be one of those classes + ApiTypeError: when the input type is not in the list of allowed spec types + """ + base_class = cls.__get_simple_class(arg) + failed_type_check_classes = cls._validate_type(base_class) + if failed_type_check_classes: + raise cls.__get_type_error( + arg, + validation_metadata.path_to_item, + failed_type_check_classes, + key_type=False, + ) + if hasattr(cls, '_validate_validations_pass'): + cls._validate_validations_pass(arg, validation_metadata) + path_to_schemas = {} + if validation_metadata.path_to_item not in path_to_schemas: + path_to_schemas[validation_metadata.path_to_item] = set() + path_to_schemas[validation_metadata.path_to_item].add(cls) + + if hasattr(cls, "_enum_by_value"): + cls._validate_enum_value(arg) + return path_to_schemas + + if base_class is none_type or base_class is bool: + return path_to_schemas + + path_to_schemas[validation_metadata.path_to_item].add(base_class) + return path_to_schemas + + @classmethod + def _validate_enum_value(cls, arg): + try: + cls._enum_by_value[arg] + except KeyError: + raise ApiValueError("Invalid value {} passed in to {}, {}".format(arg, cls, cls._enum_value_to_name)) + + @classmethod + def __get_new_cls( + cls, + arg, + validation_metadata: ValidationMetadata + ) -> typing.Dict[typing.Tuple[typing.Union[str, int], ...], 'Schema']: + """ + Make a new dynamic class and return an instance of that class + We are making an instance of cls, but instead of making cls + make a new class, new_cls + which includes dynamic bases including cls + return an instance of that new class + + Dict property + List Item Assignment Use cases: + 1. value is NOT an instance of the required schema class + the value is validated by _validate + _validate returns a key value pair + where the key is the path to the item, and the value will be the required manufactured class + made out of the matching schemas + 2. value is an instance of the the correct schema type + the value is NOT validated by _validate, _validate only checks that the instance is of the correct schema type + for this value, _validate does NOT return an entry for it in _path_to_schemas + and in list/dict _get_items,_get_properties the value will be directly assigned + because value is of the correct type, and validation was run earlier when the instance was created + """ + _path_to_schemas = cls._validate(arg, validation_metadata=validation_metadata) + # loop through it make a new class for each entry + # do not modify the returned result because it is cached and we would be modifying the cached value + path_to_schemas = {} + for path, schema_classes in _path_to_schemas.items(): + enum_schema = any( + hasattr(this_cls, '_enum_by_value') for this_cls in schema_classes) + inheritable_primitive_type = schema_classes.intersection(inheritable_primitive_types_set) + chosen_schema_classes = schema_classes + suffix = tuple() + if inheritable_primitive_type: + chosen_schema_classes = schema_classes - inheritable_primitive_types_set + if not enum_schema: + # include the inheritable_primitive_type + suffix = tuple(inheritable_primitive_type) + + if len(chosen_schema_classes) == 1 and not suffix: + mfg_cls = tuple(chosen_schema_classes)[0] + else: + x_schema = schema_descendents & chosen_schema_classes + if x_schema: + x_schema = x_schema.pop() + if any(c is not x_schema and issubclass(c, x_schema) for c in chosen_schema_classes): + # needed to not have a mro error in get_new_class + chosen_schema_classes.remove(x_schema) + used_classes = tuple(sorted(chosen_schema_classes, key=lambda a_cls: a_cls.__name__)) + suffix + mfg_cls = get_new_class(class_name='DynamicSchema', bases=used_classes) + + if inheritable_primitive_type and not enum_schema: + path_to_schemas[path] = mfg_cls + continue + + # Use case: value is None, True, False, or an enum value + value = arg + for key in path[1:]: + # if path is bigger than one, get the value that mfg_cls validated + value = value[key] + if hasattr(mfg_cls, '_enum_by_value'): + mfg_cls = mfg_cls._enum_by_value[value] + elif value in {True, False}: + mfg_cls = mfg_cls._class_by_base_class(bool) + elif value is None: + mfg_cls = mfg_cls._class_by_base_class(none_type) + else: + raise ApiValueError('Unhandled case value={} bases={}'.format(value, mfg_cls.__bases__)) + path_to_schemas[path] = mfg_cls + + return path_to_schemas + + @classmethod + def _get_new_instance_without_conversion( + cls: 'Schema', + arg: typing.Any, + path_to_item: typing.Tuple[typing.Union[str, int], ...], + path_to_schemas: typing.Dict[typing.Tuple[typing.Union[str, int], ...], typing.Type['Schema']] + ): + # We have a Dynamic class and we are making an instance of it + if issubclass(cls, frozendict): + properties = cls._get_properties(arg, path_to_item, path_to_schemas) + return super(Schema, cls).__new__(cls, properties) + elif issubclass(cls, tuple): + items = cls._get_items(arg, path_to_item, path_to_schemas) + return super(Schema, cls).__new__(cls, items) + """ + str = openapi str, date, and datetime + decimal.Decimal = openapi int and float + FileIO = openapi binary type and the user inputs a file + bytes = openapi binary type and the user inputs bytes + """ + return super(Schema, cls).__new__(cls, arg) + + @classmethod + def _from_openapi_data( + cls, + arg: typing.Union[ + str, + date, + datetime, + int, + float, + decimal.Decimal, + bool, + None, + 'Schema', + dict, + frozendict, + tuple, + list, + io.FileIO, + io.BufferedReader, + bytes + ], + _configuration: typing.Optional[Configuration] + ): + """ + Schema _from_openapi_data + """ + arg = cast_to_allowed_types(arg, from_server=True) + validation_metadata = ValidationMetadata(from_server=True, configuration=_configuration) + path_to_schemas = cls.__get_new_cls(arg, validation_metadata) + new_cls = path_to_schemas[validation_metadata.path_to_item] + new_inst = new_cls._get_new_instance_without_conversion( + arg, + validation_metadata.path_to_item, + path_to_schemas + ) + return new_inst + + @staticmethod + def __get_input_dict(*args, **kwargs) -> frozendict: + input_dict = {} + if args and isinstance(args[0], (dict, frozendict)): + input_dict.update(args[0]) + if kwargs: + input_dict.update(kwargs) + return frozendict(input_dict) + + @staticmethod + def __remove_unsets(kwargs): + return {key: val for key, val in kwargs.items() if val is not unset} + + def __new__(cls, *args: typing.Union[dict, frozendict, list, tuple, decimal.Decimal, float, int, str, date, datetime, bool, None, 'Schema'], _configuration: typing.Optional[Configuration] = None, **kwargs: typing.Union[dict, frozendict, list, tuple, decimal.Decimal, float, int, str, date, datetime, bool, None, 'Schema', Unset]): + """ + Schema __new__ + + Args: + args (int/float/decimal.Decimal/str/list/tuple/dict/frozendict/bool/None): the value + kwargs (str, int/float/decimal.Decimal/str/list/tuple/dict/frozendict/bool/None): dict values + _configuration: contains the Configuration that enables json schema validation keywords + like minItems, minLength etc + """ + kwargs = cls.__remove_unsets(kwargs) + if not args and not kwargs: + raise TypeError( + 'No input given. args or kwargs must be given.' + ) + if not kwargs and args and not isinstance(args[0], dict): + arg = args[0] + else: + arg = cls.__get_input_dict(*args, **kwargs) + validation_metadata = ValidationMetadata(configuration=_configuration, from_server=False) + arg = cast_to_allowed_types(arg, from_server=validation_metadata.from_server) + path_to_schemas = cls.__get_new_cls(arg, validation_metadata) + new_cls = path_to_schemas[validation_metadata.path_to_item] + return new_cls._get_new_instance_without_conversion( + arg, + validation_metadata.path_to_item, + path_to_schemas + ) + + def __init__( + self, + *args: typing.Union[ + dict, frozendict, list, tuple, decimal.Decimal, float, int, str, date, datetime, bool, None, 'Schema'], + _configuration: typing.Optional[Configuration] = None, + **kwargs: typing.Union[ + dict, frozendict, list, tuple, decimal.Decimal, float, int, str, date, datetime, bool, None, 'Schema', Unset + ] + ): + """ + this is needed to fix 'Unexpected argument' warning in pycharm + this code does nothing because all Schema instances are immutable + this means that all input data is passed into and used in new, and after the new instance is made + no new attributes are assigned and init is not used + """ + pass + + +def cast_to_allowed_types(arg: typing.Union[str, date, datetime, decimal.Decimal, int, float, None, dict, frozendict, list, tuple, bytes, Schema], from_server=False) -> typing.Union[str, bytes, decimal.Decimal, None, frozendict, tuple, Schema]: + """ + from_server=False date, datetime -> str + int, float -> Decimal + StrSchema will convert that to bytes and remember the encoding when we pass in str input + """ + if isinstance(arg, str): + return arg + elif type(arg) is dict or type(arg) is frozendict: + return frozendict({key: cast_to_allowed_types(val) for key, val in arg.items()}) + elif isinstance(arg, bool): + """ + this check must come before isinstance(arg, (int, float)) + because isinstance(True, int) is True + """ + return arg + elif isinstance(arg, int): + return decimal.Decimal(arg) + elif isinstance(arg, float): + decimal_from_float = decimal.Decimal(arg) + if decimal_from_float.as_integer_ratio()[1] == 1: + # 9.0 -> Decimal('9.0') + # 3.4028234663852886e+38 -> Decimal('340282346638528859811704183484516925440.0') + return decimal.Decimal(str(decimal_from_float)+'.0') + return decimal_from_float + elif type(arg) is list or type(arg) is tuple: + return tuple([cast_to_allowed_types(item) for item in arg]) + elif arg is None: + return arg + elif isinstance(arg, (date, datetime)): + if not from_server: + return arg.isoformat() + # ApiTypeError will be thrown later by _validate_type + return arg + elif isinstance(arg, uuid.UUID): + if not from_server: + return str(arg) + # ApiTypeError will be thrown later by _validate_type + return arg + elif isinstance(arg, decimal.Decimal): + return arg + elif isinstance(arg, bytes): + return arg + elif isinstance(arg, decimal.Decimal): + return arg + elif isinstance(arg, (io.FileIO, io.BufferedReader)): + if arg.closed: + raise ApiValueError('Invalid file state; file is closed and must be open') + return arg + elif isinstance(arg, Schema): + return arg + raise ValueError('Invalid type passed in got input={} type={}'.format(arg, type(arg))) + + +class ComposedBase(Discriminable): + + @classmethod + def __get_allof_classes(cls, arg, validation_metadata: ValidationMetadata): + path_to_schemas = defaultdict(set) + for allof_cls in cls._composed_schemas['allOf']: + if allof_cls in validation_metadata.base_classes: + continue + other_path_to_schemas = allof_cls._validate(arg, validation_metadata=validation_metadata) + update(path_to_schemas, other_path_to_schemas) + return path_to_schemas + + @classmethod + def __get_oneof_class( + cls, + arg, + discriminated_cls, + validation_metadata: ValidationMetadata, + path_to_schemas: typing.Dict[typing.Tuple, typing.Set[typing.Type[Schema]]] + ): + oneof_classes = [] + chosen_oneof_cls = None + original_base_classes = validation_metadata.base_classes + new_base_classes = validation_metadata.base_classes + path_to_schemas = defaultdict(set) + for oneof_cls in cls._composed_schemas['oneOf']: + if oneof_cls in path_to_schemas[validation_metadata.path_to_item]: + oneof_classes.append(oneof_cls) + continue + if isinstance(arg, oneof_cls): + # passed in instance is the correct type + chosen_oneof_cls = oneof_cls + oneof_classes.append(oneof_cls) + continue + try: + path_to_schemas = oneof_cls._validate(arg, validation_metadata=validation_metadata) + new_base_classes = validation_metadata.base_classes + except (ApiValueError, ApiTypeError) as ex: + if discriminated_cls is not None and oneof_cls is discriminated_cls: + raise ex + continue + chosen_oneof_cls = oneof_cls + oneof_classes.append(oneof_cls) + if not oneof_classes: + raise ApiValueError( + "Invalid inputs given to generate an instance of {}. None " + "of the oneOf schemas matched the input data.".format(cls) + ) + elif len(oneof_classes) > 1: + raise ApiValueError( + "Invalid inputs given to generate an instance of {}. Multiple " + "oneOf schemas {} matched the inputs, but a max of one is allowed.".format(cls, oneof_classes) + ) + return path_to_schemas + + @classmethod + def __get_anyof_classes( + cls, + arg, + discriminated_cls, + validation_metadata: ValidationMetadata + ): + anyof_classes = [] + chosen_anyof_cls = None + original_base_classes = validation_metadata.base_classes + path_to_schemas = defaultdict(set) + for anyof_cls in cls._composed_schemas['anyOf']: + if anyof_cls in validation_metadata.base_classes: + continue + if isinstance(arg, anyof_cls): + # passed in instance is the correct type + chosen_anyof_cls = anyof_cls + anyof_classes.append(anyof_cls) + continue + + try: + other_path_to_schemas = anyof_cls._validate(arg, validation_metadata=validation_metadata) + except (ApiValueError, ApiTypeError) as ex: + if discriminated_cls is not None and anyof_cls is discriminated_cls: + raise ex + continue + original_base_classes = validation_metadata.base_classes + chosen_anyof_cls = anyof_cls + anyof_classes.append(anyof_cls) + update(path_to_schemas, other_path_to_schemas) + if not anyof_classes: + raise ApiValueError( + "Invalid inputs given to generate an instance of {}. None " + "of the anyOf schemas matched the input data.".format(cls) + ) + return path_to_schemas + + @classmethod + def _validate( + cls, + arg, + validation_metadata: ValidationMetadata, + ): + """ + ComposedBase _validate + We return dynamic classes of different bases depending upon the inputs + This makes it so: + - the returned instance is always a subclass of our defining schema + - this allows us to check type based on whether an instance is a subclass of a schema + - the returned instance is a serializable type (except for None, True, and False) which are enums + + Returns: + new_cls (type): the new class + + Raises: + ApiValueError: when a string can't be converted into a date or datetime and it must be one of those classes + ApiTypeError: when the input type is not in the list of allowed spec types + """ + if isinstance(arg, Schema) and validation_metadata.from_server is False: + if isinstance(arg, cls): + # an instance of the correct type was passed in + return {} + raise ApiTypeError( + 'Incorrect type passed in, required type was {} and passed type was {} at {}'.format( + cls, + type(arg), + validation_metadata.path_to_item + ) + ) + + # validation checking on types, validations, and enums + path_to_schemas = super()._validate(arg, validation_metadata=validation_metadata) + + updated_vm = ValidationMetadata( + configuration=validation_metadata.configuration, + from_server=validation_metadata.from_server, + path_to_item=validation_metadata.path_to_item, + base_classes=validation_metadata.base_classes | frozenset({cls}) + ) + + # process composed schema + _discriminator = getattr(cls, '_discriminator', None) + discriminated_cls = None + if _discriminator and arg and isinstance(arg, frozendict): + disc_property_name = list(_discriminator.keys())[0] + cls._ensure_discriminator_value_present(disc_property_name, updated_vm, arg) + # get discriminated_cls by looking at the dict in the current class + discriminated_cls = cls._get_discriminated_class( + disc_property_name=disc_property_name, disc_payload_value=arg[disc_property_name]) + if discriminated_cls is None: + raise ApiValueError( + "Invalid discriminator value '{}' was passed in to {}.{} Only the values {} are allowed at {}".format( + arg[disc_property_name], + cls.__name__, + disc_property_name, + list(_discriminator[disc_property_name].keys()), + updated_vm.path_to_item + (disc_property_name,) + ) + ) + + if cls._composed_schemas['allOf']: + other_path_to_schemas = cls.__get_allof_classes(arg, validation_metadata=updated_vm) + update(path_to_schemas, other_path_to_schemas) + if cls._composed_schemas['oneOf']: + other_path_to_schemas = cls.__get_oneof_class( + arg, + discriminated_cls=discriminated_cls, + validation_metadata=updated_vm, + path_to_schemas=path_to_schemas + ) + update(path_to_schemas, other_path_to_schemas) + if cls._composed_schemas['anyOf']: + other_path_to_schemas = cls.__get_anyof_classes( + arg, + discriminated_cls=discriminated_cls, + validation_metadata=updated_vm + ) + update(path_to_schemas, other_path_to_schemas) + not_cls = cls._composed_schemas['not'] + if not_cls: + other_path_to_schemas = None + try: + other_path_to_schemas = not_cls._validate(arg, validation_metadata=updated_vm) + except (ApiValueError, ApiTypeError): + pass + if other_path_to_schemas: + raise ApiValueError( + "Invalid value '{}' was passed in to {}. Value is invalid because it is disallowed by {}".format( + arg, + cls.__name__, + not_cls.__name__, + ) + ) + + if discriminated_cls is not None: + # TODO use an exception from this package here + assert discriminated_cls in path_to_schemas[updated_vm.path_to_item] + return path_to_schemas + + +# DictBase, ListBase, NumberBase, StrBase, BoolBase, NoneBase +class ComposedSchema( + _SchemaTypeChecker(typing.Union[none_type, str, decimal.Decimal, bool, tuple, frozendict]), + ComposedBase, + DictBase, + ListBase, + NumberBase, + StrBase, + BoolBase, + NoneBase, + Schema +): + + # subclass properties + _composed_schemas = {} + + @classmethod + def _from_openapi_data(cls, *args: typing.Any, _configuration: typing.Optional[Configuration] = None, **kwargs): + if not args: + if not kwargs: + raise ApiTypeError('{} is missing required input data in args or kwargs'.format(cls.__name__)) + args = (kwargs, ) + return super()._from_openapi_data(args[0], _configuration=_configuration) + + +class ListSchema( + _SchemaTypeChecker(typing.Union[tuple]), + ListBase, + Schema +): + + @classmethod + def _from_openapi_data(cls, arg: typing.List[typing.Any], _configuration: typing.Optional[Configuration] = None): + return super()._from_openapi_data(arg, _configuration=_configuration) + + def __new__(cls, arg: typing.Union[list, tuple], **kwargs: ValidationMetadata): + return super().__new__(cls, arg, **kwargs) + + +class NoneSchema( + _SchemaTypeChecker(typing.Union[none_type]), + NoneBase, + Schema +): + + @classmethod + def _from_openapi_data(cls, arg: None, _configuration: typing.Optional[Configuration] = None): + return super()._from_openapi_data(arg, _configuration=_configuration) + + def __new__(cls, arg: None, **kwargs: typing.Union[ValidationMetadata]): + return super().__new__(cls, arg, **kwargs) + + +class NumberSchema( + _SchemaTypeChecker(typing.Union[decimal.Decimal]), + NumberBase, + Schema +): + """ + This is used for type: number with no format + Both integers AND floats are accepted + """ + + @classmethod + def _from_openapi_data(cls, arg: typing.Union[int, float, decimal.Decimal], _configuration: typing.Optional[Configuration] = None): + return super()._from_openapi_data(arg, _configuration=_configuration) + + def __new__(cls, arg: typing.Union[decimal.Decimal, int, float], **kwargs: typing.Union[ValidationMetadata]): + return super().__new__(cls, arg, **kwargs) + + +class IntBase(NumberBase): + @property + def as_int(self) -> int: + try: + return self._as_int + except AttributeError: + self._as_int = int(self) + return self._as_int + + @classmethod + def _validate_format(cls, arg: typing.Optional[decimal.Decimal], validation_metadata: ValidationMetadata): + if isinstance(arg, decimal.Decimal): + + denominator = arg.as_integer_ratio()[-1] + if denominator != 1: + raise ApiValueError( + "Invalid value '{}' for type integer at {}".format(arg, validation_metadata.path_to_item) + ) + + @classmethod + def _validate( + cls, + arg, + validation_metadata: ValidationMetadata, + ): + """ + IntBase _validate + TODO what about types = (int, number) -> IntBase, NumberBase? We could drop int and keep number only + """ + cls._validate_format(arg, validation_metadata=validation_metadata) + return super()._validate(arg, validation_metadata=validation_metadata) + + +class IntSchema(IntBase, NumberSchema): + + @classmethod + def _from_openapi_data(cls, arg: int, _configuration: typing.Optional[Configuration] = None): + return super()._from_openapi_data(arg, _configuration=_configuration) + + def __new__(cls, arg: typing.Union[decimal.Decimal, int], **kwargs: typing.Union[ValidationMetadata]): + return super().__new__(cls, arg, **kwargs) + + +class Int32Base( + _SchemaValidator( + inclusive_minimum=decimal.Decimal(-2147483648), + inclusive_maximum=decimal.Decimal(2147483647) + ), +): + pass + + +class Int32Schema( + Int32Base, + IntSchema +): + pass + + +class Int64Base( + _SchemaValidator( + inclusive_minimum=decimal.Decimal(-9223372036854775808), + inclusive_maximum=decimal.Decimal(9223372036854775807) + ), +): + pass + + +class Int64Schema( + Int64Base, + IntSchema +): + pass + + +class Float32Base( + _SchemaValidator( + inclusive_minimum=decimal.Decimal(-3.4028234663852886e+38), + inclusive_maximum=decimal.Decimal(3.4028234663852886e+38) + ), +): + pass + + +class Float32Schema( + Float32Base, + NumberSchema +): + + @classmethod + def _from_openapi_data(cls, arg: typing.Union[float, decimal.Decimal], _configuration: typing.Optional[Configuration] = None): + # todo check format + return super()._from_openapi_data(arg, _configuration=_configuration) + + +class Float64Base( + _SchemaValidator( + inclusive_minimum=decimal.Decimal(-1.7976931348623157E+308), + inclusive_maximum=decimal.Decimal(1.7976931348623157E+308) + ), +): + pass + + +class Float64Schema( + Float64Base, + NumberSchema +): + + @classmethod + def _from_openapi_data(cls, arg: typing.Union[float, decimal.Decimal], _configuration: typing.Optional[Configuration] = None): + # todo check format + return super()._from_openapi_data(arg, _configuration=_configuration) + + +class StrSchema( + _SchemaTypeChecker(typing.Union[str]), + StrBase, + Schema +): + """ + date + datetime string types must inherit from this class + That is because one can validate a str payload as both: + - type: string (format unset) + - type: string, format: date + """ + + @classmethod + def _from_openapi_data(cls, arg: typing.Union[str], _configuration: typing.Optional[Configuration] = None) -> 'StrSchema': + return super()._from_openapi_data(arg, _configuration=_configuration) + + def __new__(cls, arg: typing.Union[str, date, datetime, uuid.UUID], **kwargs: typing.Union[ValidationMetadata]): + return super().__new__(cls, arg, **kwargs) + + +class UUIDSchema(UUIDBase, StrSchema): + + def __new__(cls, arg: typing.Union[str, uuid.UUID], **kwargs: typing.Union[ValidationMetadata]): + return super().__new__(cls, arg, **kwargs) + + +class DateSchema(DateBase, StrSchema): + + def __new__(cls, arg: typing.Union[str, datetime], **kwargs: typing.Union[ValidationMetadata]): + return super().__new__(cls, arg, **kwargs) + + +class DateTimeSchema(DateTimeBase, StrSchema): + + def __new__(cls, arg: typing.Union[str, datetime], **kwargs: typing.Union[ValidationMetadata]): + return super().__new__(cls, arg, **kwargs) + + +class DecimalSchema(DecimalBase, StrSchema): + + def __new__(cls, arg: typing.Union[str], **kwargs: typing.Union[ValidationMetadata]): + """ + Note: Decimals may not be passed in because cast_to_allowed_types is only invoked once for payloads + which can be simple (str) or complex (dicts or lists with nested values) + Because casting is only done once and recursively casts all values prior to validation then for a potential + client side Decimal input if Decimal was accepted as an input in DecimalSchema then one would not know + if one was using it for a StrSchema (where it should be cast to str) or one is using it for NumberSchema + where it should stay as Decimal. + """ + return super().__new__(cls, arg, **kwargs) + + +class BytesSchema( + _SchemaTypeChecker(typing.Union[bytes]), + Schema, +): + """ + this class will subclass bytes and is immutable + """ + def __new__(cls, arg: typing.Union[bytes], **kwargs: typing.Union[ValidationMetadata]): + return super(Schema, cls).__new__(cls, arg) + + +class FileSchema( + _SchemaTypeChecker(typing.Union[FileIO]), + Schema, +): + """ + This class is NOT immutable + Dynamic classes are built using it for example when AnyType allows in binary data + Al other schema classes ARE immutable + If one wanted to make this immutable one could make this a DictSchema with required properties: + - data = BytesSchema (which would be an immutable bytes based schema) + - file_name = StrSchema + and cast_to_allowed_types would convert bytes and file instances into dicts containing data + file_name + The downside would be that data would be stored in memory which one may not want to do for very large files + + The developer is responsible for closing this file and deleting it + + This class was kept as mutable: + - to allow file reading and writing to disk + - to be able to preserve file name info + """ + + def __new__(cls, arg: typing.Union[io.FileIO, io.BufferedReader], **kwargs: typing.Union[ValidationMetadata]): + return super(Schema, cls).__new__(cls, arg) + + +class BinaryBase: + pass + + +class BinarySchema( + _SchemaTypeChecker(typing.Union[bytes, FileIO]), + ComposedBase, + BinaryBase, + Schema, +): + + @classmethod + @property + def _composed_schemas(cls): + # we need this here to make our import statements work + # we must store _composed_schemas in here so the code is only run + # when we invoke this method. If we kept this at the class + # level we would get an error because the class level + # code would be run when this module is imported, and these composed + # classes don't exist yet because their module has not finished + # loading + return { + 'allOf': [], + 'oneOf': [ + BytesSchema, + FileSchema, + ], + 'anyOf': [ + ], + 'not': None + } + + def __new__(cls, arg: typing.Union[io.FileIO, io.BufferedReader, bytes], **kwargs: typing.Union[ValidationMetadata]): + return super().__new__(cls, arg) + + +class BoolSchema( + _SchemaTypeChecker(typing.Union[bool]), + BoolBase, + Schema +): + + @classmethod + def _from_openapi_data(cls, arg: bool, _configuration: typing.Optional[Configuration] = None): + return super()._from_openapi_data(arg, _configuration=_configuration) + + def __new__(cls, arg: bool, **kwargs: typing.Union[ValidationMetadata]): + return super().__new__(cls, arg, **kwargs) + + +class AnyTypeSchema( + _SchemaTypeChecker( + typing.Union[frozendict, tuple, decimal.Decimal, str, bool, none_type, bytes, FileIO] + ), + DictBase, + ListBase, + NumberBase, + StrBase, + BoolBase, + NoneBase, + Schema +): + pass + + +class DictSchema( + _SchemaTypeChecker(typing.Union[frozendict]), + DictBase, + Schema +): + + @classmethod + def _from_openapi_data(cls, arg: typing.Dict[str, typing.Any], _configuration: typing.Optional[Configuration] = None): + return super()._from_openapi_data(arg, _configuration=_configuration) + + def __new__(cls, *args: typing.Union[dict, frozendict], **kwargs: typing.Union[dict, frozendict, list, tuple, decimal.Decimal, float, int, str, date, datetime, bool, None, bytes, Schema, Unset, ValidationMetadata]): + return super().__new__(cls, *args, **kwargs) + + +schema_descendents = set([NoneSchema, DictSchema, ListSchema, NumberSchema, StrSchema, BoolSchema]) + + +def deserialize_file(response_data, configuration, content_disposition=None): + """Deserializes body to file + + Saves response body into a file in a temporary folder, + using the filename from the `Content-Disposition` header if provided. + + Args: + param response_data (str): the file data to write + configuration (Configuration): the instance to use to convert files + + Keyword Args: + content_disposition (str): the value of the Content-Disposition + header + + Returns: + (file_type): the deserialized file which is open + The user is responsible for closing and reading the file + """ + fd, path = tempfile.mkstemp(dir=configuration.temp_folder_path) + os.close(fd) + os.remove(path) + + if content_disposition: + filename = re.search(r'filename=[\'"]?([^\'"\s]+)[\'"]?', + content_disposition).group(1) + path = os.path.join(os.path.dirname(path), filename) + + with open(path, "wb") as f: + if isinstance(response_data, str): + # change str to bytes so we can write it + response_data = response_data.encode('utf-8') + f.write(response_data) + + f = open(path, "rb") + return f + + +@functools.cache +def get_new_class( + class_name: str, + bases: typing.Tuple[typing.Type[typing.Union[Schema, typing.Any]], ...] +) -> typing.Type[Schema]: + """ + Returns a new class that is made with the subclass bases + """ + return type(class_name, bases, {}) + + +LOG_CACHE_USAGE = False + + +def log_cache_usage(cache_fn): + if LOG_CACHE_USAGE: + print(cache_fn.__name__, cache_fn.cache_info())