diff --git a/CHANGELOG.md b/CHANGELOG.md index f59fffa6..3318284a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - support `` flag to exclude abstract types (not interfaces) - support `` flag to exclude interface types +### `jsonschema-module-microprofile-openapi-3` +#### Added +***NOTE: `org.eclipse.microprofile.openapi:microprofile-openapi-api` minimum version is `3.1.1`!*** +- Initial implementation of `MicroProfileOpenApi3Module` for deriving schema attributes from MicroProfile OpenAPI 3 `@Schema` annotations. + ## [4.36.0] - 2024-07-20 ### `jsonschema-generator` #### Added diff --git a/jsonschema-examples/pom.xml b/jsonschema-examples/pom.xml index b5dc5d8d..03bc45f3 100644 --- a/jsonschema-examples/pom.xml +++ b/jsonschema-examples/pom.xml @@ -64,6 +64,15 @@ io.swagger.core.v3 swagger-annotations + + + com.github.victools + jsonschema-module-microprofile-openapi-3 + + + org.eclipse.microprofile.openapi + microprofile-openapi-api + io.github.classgraph classgraph diff --git a/jsonschema-generator-bom/pom.xml b/jsonschema-generator-bom/pom.xml index 4e7b165e..47ad3a6b 100644 --- a/jsonschema-generator-bom/pom.xml +++ b/jsonschema-generator-bom/pom.xml @@ -84,6 +84,11 @@ jsonschema-module-swagger-2 ${project.version} + + com.github.victools + jsonschema-module-microprofile-openapi-3 + ${project.version} + diff --git a/jsonschema-generator-parent/pom.xml b/jsonschema-generator-parent/pom.xml index af5d7a9c..ca869cda 100644 --- a/jsonschema-generator-parent/pom.xml +++ b/jsonschema-generator-parent/pom.xml @@ -164,6 +164,7 @@ 2.0.1.Final 1.6.7 2.2.5 + 3.1.1 4.8.149 @@ -212,6 +213,11 @@ classgraph ${version.classgraph} + + org.eclipse.microprofile.openapi + microprofile-openapi-api + ${version.microprofile-openapi-3} + diff --git a/jsonschema-maven-plugin/README.md b/jsonschema-maven-plugin/README.md index 8f7f9927..03b58236 100644 --- a/jsonschema-maven-plugin/README.md +++ b/jsonschema-maven-plugin/README.md @@ -187,12 +187,13 @@ When you want to have more control over the modules that are to be used during g ``` This configuration will generate the schema using the Jackson module. -There are five standard modules that can be used: +There are six standard modules that can be used: - `Jackson` - `JakartaValidation` - `JavaxValidation` - `Swagger15` - `Swagger2` +- `MicroProfileOpenApi3` ### Defining options for a module ```xml diff --git a/jsonschema-maven-plugin/pom.xml b/jsonschema-maven-plugin/pom.xml index 408d211f..d36a3f26 100644 --- a/jsonschema-maven-plugin/pom.xml +++ b/jsonschema-maven-plugin/pom.xml @@ -129,6 +129,15 @@ io.swagger.core.v3 swagger-annotations + + + com.github.victools + jsonschema-module-microprofile-openapi-3 + + + org.eclipse.microprofile.openapi + microprofile-openapi-api + diff --git a/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/SchemaGeneratorMojo.java b/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/SchemaGeneratorMojo.java index 272bfa65..2c5a4441 100644 --- a/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/SchemaGeneratorMojo.java +++ b/jsonschema-maven-plugin/src/main/java/com/github/victools/jsonschema/plugin/maven/SchemaGeneratorMojo.java @@ -31,6 +31,7 @@ import com.github.victools.jsonschema.module.jakarta.validation.JakartaValidationOption; import com.github.victools.jsonschema.module.javax.validation.JavaxValidationModule; import com.github.victools.jsonschema.module.javax.validation.JavaxValidationOption; +import com.github.victools.jsonschema.module.microprofile.openapi3.MicroProfileOpenApi3Module; import com.github.victools.jsonschema.module.swagger15.SwaggerModule; import com.github.victools.jsonschema.module.swagger15.SwaggerOption; import com.github.victools.jsonschema.module.swagger2.Swagger2Module; @@ -522,9 +523,14 @@ private void addStandardModule(GeneratorModule module, SchemaGeneratorConfigBuil this.getLog().debug("- Adding Swagger 2.x Module"); configBuilder.with(new Swagger2Module()); break; + case "MicroProfileOpenApi3": + this.getLog().debug("- Adding MicroProfile OpenAPI 3.x Module"); + configBuilder.with(new MicroProfileOpenApi3Module()); + break; default: throw new MojoExecutionException("Error: Module does not have a name in " - + "['Jackson', 'JakartaValidation', 'JavaxValidation', 'Swagger15', 'Swagger2'] or does not have a custom classname."); + + "['Jackson', 'JakartaValidation', 'JavaxValidation', 'Swagger15', 'Swagger2', 'MicroProfileOpenApi3']" + + " or does not have a custom classname."); } } diff --git a/jsonschema-maven-plugin/src/test/java/com/github/victools/jsonschema/plugin/maven/SchemaGeneratorMojoTest.java b/jsonschema-maven-plugin/src/test/java/com/github/victools/jsonschema/plugin/maven/SchemaGeneratorMojoTest.java index 010ea79c..83723386 100644 --- a/jsonschema-maven-plugin/src/test/java/com/github/victools/jsonschema/plugin/maven/SchemaGeneratorMojoTest.java +++ b/jsonschema-maven-plugin/src/test/java/com/github/victools/jsonschema/plugin/maven/SchemaGeneratorMojoTest.java @@ -57,6 +57,7 @@ public void setUp() throws Exception { "JakartaValidationModule", "Swagger15Module", "Swagger2Module", + "MicroProfileOpenApi3Module", "Complete" }) public void testGeneration(String testCaseName) throws Exception { diff --git a/jsonschema-maven-plugin/src/test/resources/reference-test-cases/MicroProfileOpenApi3Module-pom.xml b/jsonschema-maven-plugin/src/test/resources/reference-test-cases/MicroProfileOpenApi3Module-pom.xml new file mode 100644 index 00000000..181b6901 --- /dev/null +++ b/jsonschema-maven-plugin/src/test/resources/reference-test-cases/MicroProfileOpenApi3Module-pom.xml @@ -0,0 +1,19 @@ + + + + + com.github.victools + jsonschema-maven-plugin + + com.github.victools.jsonschema.plugin.maven.TestClass + target/generated-test-sources/MicroProfileOpenApi3Module + + + MicroProfileOpenApi3 + + + + + + + \ No newline at end of file diff --git a/jsonschema-maven-plugin/src/test/resources/reference-test-cases/MicroProfileOpenApi3Module-reference.json b/jsonschema-maven-plugin/src/test/resources/reference-test-cases/MicroProfileOpenApi3Module-reference.json new file mode 100644 index 00000000..d3596b57 --- /dev/null +++ b/jsonschema-maven-plugin/src/test/resources/reference-test-cases/MicroProfileOpenApi3Module-reference.json @@ -0,0 +1,9 @@ +{ + "$schema" : "http://json-schema.org/draft-07/schema#", + "type" : "object", + "properties" : { + "anInt" : { + "type" : "integer" + } + } +} \ No newline at end of file diff --git a/jsonschema-module-microprofile-openapi-3/README.md b/jsonschema-module-microprofile-openapi-3/README.md new file mode 100644 index 00000000..f480a28b --- /dev/null +++ b/jsonschema-module-microprofile-openapi-3/README.md @@ -0,0 +1,94 @@ +# Java JSON Schema Generator – Module Microprofile OpenAPI (3.x) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.victools/jsonschema-module-microprofile-openapi-3/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.github.victools/jsonschema-module-microprofile-openapi-3) +Module for the [jsonschema-generator](../jsonschema-generator) – deriving JSON Schema attributes from `microprofile-openapi-api` (3.x) annotations + +## Features + 1. From `@Schema(description = …)` on types in general, derive `"description"`. + 2. From `@Schema(title = …)` on types in general, derive `"title"`. + 3. From `@Schema(ref = …)` on types in general, replace subschema with `"$ref"` to external/separate schema (except for the main type being targeted). + 4. From `@Schema(subTypes = …)` on types in general, derive `"anyOf"` alternatives. + 5. From `@Schema(anyOf = …)` on types in general (as alternative to `subTypes`), derive `"anyOf"` alternatives. + 6. From `@Schema(name = …)` on types in general, derive the keys/names in `"definitions"`/`"$defs"`. + 7. From `@Schema(description = …)` on fields/methods, derive `"description"`. + 8. From `@Schema(title = …)` on fields/methods, derive `"title"`. + 9. From `@Schema(implementation = …)` on fields/methods, override represented type. +10. From `@Schema(hidden = true)` on fields/methods, skip certain properties. +11. From `@Schema(name = …)` on fields/methods, override property names. +12. From `@Schema(ref = …)` on fields/methods, replace subschema with `"$ref"` to external/separate schema. +13. From `@Schema(allOf = …)` on fields/methods, include `"allOf"` parts. +14. From `@Schema(anyOf = …)` on fields/methods, include `"anyOf"` parts. +15. From `@Schema(oneOf = …)` on fields/methods, include `"oneOf"` parts. +16. From `@Schema(not = …)` on fields/methods, include the indicated `"not"` subschema. +17. From `@Schema(required = true)` on fields/methods, mark property as `"required"` in the schema containing the property. +18. From `@Schema(requiredProperties = …)` on fields/methods, derive its `"required"` properties. +19. From `@Schema(minProperties = …)` on fields/methods, derive its `"minProperties"`. +20. From `@Schema(maxProperties = …)` on fields/methods, derive its `"maxProperties"`. +21. From `@Schema(nullable = true)` on fields/methods, include `null` in its `"type"` - when schema type is ARRAY, applied on array item only. +22. From `@Schema(allowableValues = …)` on fields/methods, derive its `"const"`/`"enum"`. +23. From `@Schema(defaultValue = …)` on fields/methods, derive its `"default"` - when schema type is ARRAY, applied on array item only. +24. From `@Schema(readOnly = …)` on fields/methods, to mark them as `"readOnly"`. +25. From `@Schema(writeOnly = …)` on fields/methods, to mark them as `"writeOnly"`. +26. From `@Schema(minLength = …)` on fields/methods, derive its `"minLength"`. +27. From `@Schema(maxLength = …)` on fields/methods, derive its `"maxLength"`. +28. From `@Schema(format = …)` on fields/methods, derive its `"format"`. +29. From `@Schema(pattern = …)` on fields/methods, derive its `"pattern"`. +30. From `@Schema(multipleOf = …)` on fields/methods, derive its `"multipleOf"`. +31. From `@Schema(minimum = …, exclusiveMinimum = …)` on fields/methods, derive its `"minimum"`/`"exclusiveMinimum"`. +32. From `@Schema(maximum = …, exclusiveMaximum = …)` on fields/methods, derive its `"maximum"`/`"exclusiveMaximum"`. +33. From `@Schema(minItems = …)` on fields/methods when schema type is ARRAY, derive its `"minItems"`. +34. From `@Schema(maxItems = …)` on fields/methods when schema type is ARRAY, derive its `"maxItems"`. +35. From `@Schema(uniqueItems = …)` on fields/methods when schema type is ARRAY, derive its `"uniqueItems"`. + +Schema attributes derived from `@Schema` on fields are also applied to their respective getter methods. +Schema attributes derived from `@Schema` on getter methods are also applied to their associated fields. + +---- + +## Documentation +JavaDoc is being used throughout the codebase, offering contextual information in your respective IDE or being available online through services like [javadoc.io](https://www.javadoc.io/doc/com.github.victools/jsonschema-module-microprofile-openapi-3). + +Additional documentation can be found in the [Project Wiki](https://github.com/victools/jsonschema-generator/wiki). + +---- + +## Usage +### Dependency (Maven) +```xml + + com.github.victools + jsonschema-module-microprofile-openapi-3 + [4.37.0,) + +``` + +The release versions of the main generator library and this module are aligned. +It is recommended to use identical versions for both dependencies to ensure compatibility. + +### Code +#### Passing into SchemaGeneratorConfigBuilder.with(Module) + +```java + + +``` +```java +MicroProfileOpenApi3Module module = new MicroProfileOpenApi3Module(); +SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09) + .with(module); +``` + +#### Complete Example + +```java + +``` +```java +MicroProfileOpenApi3Module module = new MicroProfileOpenApi3Module(); +SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON) + .with(module); +SchemaGeneratorConfig config = configBuilder.build(); +SchemaGenerator generator = new SchemaGenerator(config); +JsonNode jsonSchema = generator.generateSchema(YourClass.class); + +System.out.println(jsonSchema.toString()); +``` diff --git a/jsonschema-module-microprofile-openapi-3/pom.xml b/jsonschema-module-microprofile-openapi-3/pom.xml new file mode 100644 index 00000000..5c4c9cf2 --- /dev/null +++ b/jsonschema-module-microprofile-openapi-3/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + + com.github.victools + jsonschema-generator-parent + 4.37.0-SNAPSHOT + ../jsonschema-generator-parent/pom.xml + + jsonschema-module-microprofile-openapi-3 + + Java JSON Schema Generator Module – Microprofile OpenAPI (3.x) + Module for the jsonschema-generator – Microprofile OpenAPI (3.x) + + + com.github.victools.jsonschema.module.microprofile.openapi3 + + + + + com.github.victools + jsonschema-generator + provided + + + org.eclipse.microprofile.openapi + microprofile-openapi-api + provided + + + + + + + maven-compiler-plugin + + + maven-checkstyle-plugin + + + maven-source-plugin + + + maven-javadoc-plugin + + + org.moditect + moditect-maven-plugin + + + + + diff --git a/jsonschema-module-microprofile-openapi-3/src/main/java/com/github/victools/jsonschema/module/microprofile/openapi3/ExternalRefCustomDefinitionProvider.java b/jsonschema-module-microprofile-openapi-3/src/main/java/com/github/victools/jsonschema/module/microprofile/openapi3/ExternalRefCustomDefinitionProvider.java new file mode 100644 index 00000000..7a11b3f9 --- /dev/null +++ b/jsonschema-module-microprofile-openapi-3/src/main/java/com/github/victools/jsonschema/module/microprofile/openapi3/ExternalRefCustomDefinitionProvider.java @@ -0,0 +1,58 @@ +/* + * Copyright 2024 VicTools. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.victools.jsonschema.module.microprofile.openapi3; + +import com.fasterxml.classmate.ResolvedType; +import com.github.victools.jsonschema.generator.CustomDefinition; +import com.github.victools.jsonschema.generator.CustomDefinitionProviderV2; +import com.github.victools.jsonschema.generator.SchemaGenerationContext; +import com.github.victools.jsonschema.generator.SchemaKeyword; +import java.util.Optional; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +/** + * Replace any type annotated with {@code @Schema(ref = "...")} with the specified reference value, unless it is the main schema being targeted. + */ +public class ExternalRefCustomDefinitionProvider implements CustomDefinitionProviderV2 { + + /** + * Reference to the targeted type, for which a schema is being generated, that should not be replaced by a "ref". + */ + private Class mainType; + + @Override + public CustomDefinition provideCustomSchemaDefinition(ResolvedType javaType, SchemaGenerationContext context) { + Class erasedType = javaType.getErasedType(); + if (this.mainType == null) { + this.mainType = erasedType; + } + if (this.mainType == erasedType) { + return null; + } + return Optional.ofNullable(erasedType.getAnnotation(Schema.class)) + .map(Schema::ref) + .filter(ref -> !ref.isEmpty()) + .map(ref -> context.getGeneratorConfig().createObjectNode().put(context.getKeyword(SchemaKeyword.TAG_REF), ref)) + .map(schema -> new CustomDefinition(schema, CustomDefinition.INLINE_DEFINITION, CustomDefinition.INCLUDING_ATTRIBUTES)) + .orElse(null); + } + + @Override + public void resetAfterSchemaGenerationFinished() { + this.mainType = null; + } +} diff --git a/jsonschema-module-microprofile-openapi-3/src/main/java/com/github/victools/jsonschema/module/microprofile/openapi3/MicroProfileOpenApi3AnyOfResolver.java b/jsonschema-module-microprofile-openapi-3/src/main/java/com/github/victools/jsonschema/module/microprofile/openapi3/MicroProfileOpenApi3AnyOfResolver.java new file mode 100644 index 00000000..30638167 --- /dev/null +++ b/jsonschema-module-microprofile-openapi-3/src/main/java/com/github/victools/jsonschema/module/microprofile/openapi3/MicroProfileOpenApi3AnyOfResolver.java @@ -0,0 +1,44 @@ +/* + * Copyright 2024 VicTools. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.victools.jsonschema.module.microprofile.openapi3; + +import com.fasterxml.classmate.ResolvedType; +import com.github.victools.jsonschema.generator.SchemaGenerationContext; +import com.github.victools.jsonschema.generator.SubtypeResolver; +import com.github.victools.jsonschema.generator.TypeContext; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +/** + * Subtype resolver considering {@code @Schema(anyOf = ...)}. + */ +public class MicroProfileOpenApi3AnyOfResolver implements SubtypeResolver { + + @Override + public List findSubtypes(ResolvedType declaredType, SchemaGenerationContext context) { + Schema annotation = declaredType.getErasedType().getAnnotation(Schema.class); + if (annotation == null || annotation.anyOf().length == 0) { + return null; + } + TypeContext typeContext = context.getTypeContext(); + return Stream.of(annotation.anyOf()) + .map(typeContext::resolve) + .collect(Collectors.toList()); + } +} diff --git a/jsonschema-module-microprofile-openapi-3/src/main/java/com/github/victools/jsonschema/module/microprofile/openapi3/MicroProfileOpenApi3Module.java b/jsonschema-module-microprofile-openapi-3/src/main/java/com/github/victools/jsonschema/module/microprofile/openapi3/MicroProfileOpenApi3Module.java new file mode 100644 index 00000000..ab4a3ac0 --- /dev/null +++ b/jsonschema-module-microprofile-openapi-3/src/main/java/com/github/victools/jsonschema/module/microprofile/openapi3/MicroProfileOpenApi3Module.java @@ -0,0 +1,554 @@ +/* + * Copyright 2024 VicTools. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.victools.jsonschema.module.microprofile.openapi3; + +import com.fasterxml.classmate.ResolvedType; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.victools.jsonschema.generator.ConfigFunction; +import com.github.victools.jsonschema.generator.CustomDefinition; +import com.github.victools.jsonschema.generator.CustomPropertyDefinition; +import com.github.victools.jsonschema.generator.MemberScope; +import com.github.victools.jsonschema.generator.Module; +import com.github.victools.jsonschema.generator.SchemaGenerationContext; +import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder; +import com.github.victools.jsonschema.generator.SchemaGeneratorConfigPart; +import com.github.victools.jsonschema.generator.SchemaGeneratorGeneralConfigPart; +import com.github.victools.jsonschema.generator.SchemaKeyword; +import com.github.victools.jsonschema.generator.TypeScope; +import java.lang.reflect.Type; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +/** + * JSON Schema Generator Module - MicroProfile OpenAPI (3.x). + * + * @since 4.37.0 + */ +public class MicroProfileOpenApi3Module implements Module { + @Override + public void applyToConfigBuilder(SchemaGeneratorConfigBuilder builder) { + this.applyToConfigBuilder(builder.forTypesInGeneral()); + this.applyToConfigBuilder(builder.forFields()); + this.applyToConfigBuilder(builder.forMethods()); + } + + private void applyToConfigBuilder(SchemaGeneratorGeneralConfigPart configPart) { + configPart.withDescriptionResolver(this.createTypePropertyResolver(Schema::description, description -> !description.isEmpty())); + configPart.withTitleResolver(this.createTypePropertyResolver(Schema::title, title -> !title.isEmpty())); + configPart.withAdditionalPropertiesResolver(this.createTypePropertyResolver(this::mapAdditionalPropertiesEnumValue, Objects::nonNull)); + + configPart.withCustomDefinitionProvider(new ExternalRefCustomDefinitionProvider()); + configPart.withSubtypeResolver(new MicroProfileOpenApi3AnyOfResolver()); + configPart.withDefinitionNamingStrategy(new MicroProfileOpenApi3SchemaDefinitionNamingStrategy(configPart.getDefinitionNamingStrategy())); + } + + private void applyToConfigBuilder(SchemaGeneratorConfigPart configPart) { + configPart.withTargetTypeOverridesResolver(this::resolveTargetTypeOverrides); + configPart.withIgnoreCheck(this::shouldBeIgnored); + configPart.withPropertyNameOverrideResolver(this::resolvePropertyNameOverride); + configPart.withCustomDefinitionProvider(this::provideCustomSchemaDefinition); + + configPart.withDescriptionResolver(this::resolveDescription); + configPart.withTitleResolver(this::resolveTitle); + configPart.withRequiredCheck(this::checkRequired); + configPart.withNullableCheck(this::checkNullable); + configPart.withReadOnlyCheck(this::checkReadOnly); + configPart.withWriteOnlyCheck(this::checkWriteOnly); + configPart.withEnumResolver(this::resolveEnum); + configPart.withDefaultResolver(this::resolveDefault); + + configPart.withStringMinLengthResolver(this::resolveMinLength); + configPart.withStringMaxLengthResolver(this::resolveMaxLength); + configPart.withStringFormatResolver(this::resolveFormat); + configPart.withStringPatternResolver(this::resolvePattern); + + configPart.withNumberMultipleOfResolver(this::resolveMultipleOf); + configPart.withNumberExclusiveMaximumResolver(this::resolveExclusiveMaximum); + configPart.withNumberInclusiveMaximumResolver(this::resolveInclusiveMaximum); + configPart.withNumberExclusiveMinimumResolver(this::resolveExclusiveMinimum); + configPart.withNumberInclusiveMinimumResolver(this::resolveInclusiveMinimum); + + configPart.withArrayMinItemsResolver(this::resolveArrayMinItems); + configPart.withArrayMaxItemsResolver(this::resolveArrayMaxItems); + configPart.withArrayUniqueItemsResolver(this::resolveArrayUniqueItems); + + // take care of various keywords that are not so straightforward to apply + configPart.withInstanceAttributeOverride(this::overrideInstanceAttributes); + } + + /** + * Derive target type override from {@code @Schema(implementation = ...)}. + * + * @param member field/method to determine target type override for + * @return single target type override or null + */ + protected List resolveTargetTypeOverrides(MemberScope member) { + return this.getSchemaAnnotationValue(member, Schema::implementation, annotatedImplementation -> annotatedImplementation != Void.class) + .map(annotatedType -> member.getContext().resolve(annotatedType)) + .map(Collections::singletonList) + .orElse(null); + } + + /** + * Determine whether the given field/method should be skipped, based on {@code @Schema(hidden = true)}. + * + * @param member field/method to check + * @return whether to skip the field/method + */ + protected boolean shouldBeIgnored(MemberScope member) { + return this.getSchemaAnnotationValue(member, Schema::hidden, Boolean.TRUE::equals) + .isPresent(); + } + + /** + * Determine an alternative name for the given field/method, based on {@code @Schema(name = ...)}. + * + * @param member field/method to look-up alternative property name for + * @return alternative property name + */ + protected String resolvePropertyNameOverride(MemberScope member) { + return this.getSchemaAnnotationValue(member, Schema::name, name -> !name.isEmpty()) + .orElse(null); + } + + /** + * Look-up description from {@code @Schema(description = ...)} for given field/method. + * + * @param member field/method to look-up description for + * @return schema description + */ + protected String resolveDescription(MemberScope member) { + return this.getSchemaAnnotationValue(member, Schema::description, description -> !description.isEmpty()) + .orElse(null); + } + + /** + * Look-up title from {@code @Schema(title = ...)} for given field/method. + * + * @param member field/method to look-up title for + * @return schema title + */ + protected String resolveTitle(MemberScope member) { + return this.getSchemaAnnotationValue(member, Schema::title, title -> !title.isEmpty()) + .orElse(null); + } + + /** + * Determine whether the given field/method is deemed required in its containing type based on {@code @Schema(required = true)}. + * + * @param member field/method to check + * @return whether the field/method is required + */ + protected boolean checkRequired(MemberScope member) { + return this.getSchemaAnnotationValue(member, + Schema::required, + Boolean.TRUE::equals) + .isPresent(); + } + + private boolean isArrayType(MemberScope member, Schema schema) { + return SchemaType.ARRAY.equals(schema.type()) && !member.isFakeContainerItemScope(); + } + + /** + * Determine whether the given field/method may be null based on {@code @Schema(nullable = true)}. + * + * @param member field/method to check + * @return whether the field/method is nullable + */ + protected boolean checkNullable(MemberScope member) { + return this.getSchemaAnnotationValue(member, + schema -> isArrayType(member, schema) ? null : schema.nullable(), + Boolean.TRUE::equals) + .isPresent(); + } + + /** + * Derive the allowed type of a schema's additional properties from the given annotation. + * + * @param annotation annotation to check + * @return {@code Object.class} (if true or an external "$ref" is specified), {@code Void.class} (if forbidden) or {@code null} (if undefined) + */ + protected Type mapAdditionalPropertiesEnumValue(Schema annotation) { + if (!annotation.ref().isEmpty()) { + // prevent invalid combination of "$ref" with "additionalProperties": false + return Object.class; + } + if (Schema.True.class.equals(annotation.additionalProperties())) { + // allow any additional properties + return Object.class; + } else if (Schema.False.class.equals(annotation.additionalProperties())) { + // block any additional properties + return Void.class; + } else { + return annotation.additionalProperties(); + } + } + + /** + * Determine whether the given field/method is deemed read-only based on {@code @Schema(readOnly = true)}. + * + * @param member field/method to check + * @return whether the field/method is read-only + */ + protected boolean checkReadOnly(MemberScope member) { + return this.getSchemaAnnotationValue(member, Schema::readOnly, Boolean.TRUE::equals) + .isPresent(); + } + + /** + * Determine whether the given field/method is deemed write-only based on {@code @Schema(writeOnly = true)}. + * + * @param member field/method to check + * @return whether the field/method is write-only + */ + protected boolean checkWriteOnly(MemberScope member) { + return this.getSchemaAnnotationValue(member, Schema::writeOnly, Boolean.TRUE::equals) + .isPresent(); + } + + /** + * Look-up the finite list of possible values from {@code @Schema(allowableValues = ...)}. + * + * @param member field/method to determine allowed values for + * @return applicable "const"/"enum" values or null + */ + protected List resolveEnum(MemberScope member) { + return this.getSchemaAnnotationValue(member, Schema::enumeration, values -> values.length > 0) + .map(Arrays::asList) + .orElse(null); + } + + /** + * Look-up the default value for the given field/method from {@code @Schema(defaultValue = ...)}. + * + * @param member field/method to determine default value for + * @return default property value or null + */ + protected String resolveDefault(MemberScope member) { + return this.getSchemaAnnotationValue(member, + schema -> isArrayType(member, schema) ? null : schema.defaultValue(), + defaultValue -> !defaultValue.isEmpty()) + .orElse(null); + } + + /** + * Look-up the value from {@code @Schema(minLength = ...)} for the given field/method. + * + * @param member field/method to look-up minimum string length for + * @return minimum string length or null + */ + protected Integer resolveMinLength(MemberScope member) { + return this.getSchemaAnnotationValue(member, Schema::minLength, minLength -> minLength > 0) + .orElse(null); + } + + /** + * Look-up the value from {@code @Schema(maxLength = ...)} for the given field/method. + * + * @param member field/method to look-up maximum string length for + * @return maximum string length or null + */ + protected Integer resolveMaxLength(MemberScope member) { + return this.getSchemaAnnotationValue(member, Schema::maxLength, maxLength -> maxLength < Integer.MAX_VALUE && maxLength > -1) + .orElse(null); + } + + /** + * Look-up the value from {@code @Schema(format = ...)} for the given field/method. + * + * @param member field/method to look-up format for + * @return format value or null + */ + protected String resolveFormat(MemberScope member) { + return this.getSchemaAnnotationValue(member, Schema::format, format -> !format.isEmpty()) + .orElse(null); + } + + /** + * Look-up the value from {@code @Schema(pattern = ...)} for the given field/method. + * + * @param member field/method to look-up pattern for + * @return pattern value or null + */ + protected String resolvePattern(MemberScope member) { + return this.getSchemaAnnotationValue(member, Schema::pattern, pattern -> !pattern.isEmpty()) + .orElse(null); + } + + /** + * Look-up the value from {@code @Schema(multipleOf = ...)} for the given field/method. + * + * @param member field/method to look-up multipleOf for + * @return multipleOf value or null + */ + protected BigDecimal resolveMultipleOf(MemberScope member) { + return this.getSchemaAnnotationValue(member, Schema::multipleOf, multipleOf -> multipleOf != 0) + .map(BigDecimal::valueOf) + .orElse(null); + } + + /** + * Look-up the exclusive maximum value from {@code @Schema(maximum = ..., exclusiveMaxium = true)} for the given field/method. + * + * @param member field/method to look-up exclusiveMaximum for + * @return exclusiveMaximum value or null + */ + protected BigDecimal resolveExclusiveMaximum(MemberScope member) { + return this.getSchemaAnnotationValue(member, Schema::maximum, maximum -> !maximum.isEmpty()) + .filter(_maximum -> this.getSchemaAnnotationValue(member, Schema::exclusiveMaximum, Boolean.TRUE::equals).isPresent()) + .map(BigDecimal::new) + .orElse(null); + } + + /** + * Look-up the inclusive maximum value from {@code @Schema(maximum = ..., exclusiveMaxium = false)} for the given field/method. + * + * @param member field/method to look-up maximum for + * @return maximum value or null + */ + protected BigDecimal resolveInclusiveMaximum(MemberScope member) { + return this.getSchemaAnnotationValue(member, Schema::maximum, maximum -> !maximum.isEmpty()) + .filter(_maximum -> this.getSchemaAnnotationValue(member, Schema::exclusiveMaximum, Boolean.FALSE::equals).isPresent()) + .map(BigDecimal::new) + .orElse(null); + } + + /** + * Look-up the exclusive minimum value from {@code @Schema(minimum = ..., exclusiveMinium = true)} for the given field/method. + * + * @param member field/method to look-up exclusiveMinimum for + * @return exclusiveMinimum value or null + */ + protected BigDecimal resolveExclusiveMinimum(MemberScope member) { + return this.getSchemaAnnotationValue(member, Schema::minimum, minimum -> !minimum.isEmpty()) + .filter(_minimum -> this.getSchemaAnnotationValue(member, Schema::exclusiveMinimum, Boolean.TRUE::equals).isPresent()) + .map(BigDecimal::new) + .orElse(null); + } + + /** + * Look-up the inclusive minimum value from {@code @Schema(minimum = ..., exclusiveMinium = false)} for the given field/method. + * + * @param member field/method to look-up minimum for + * @return minimum value or null + */ + protected BigDecimal resolveInclusiveMinimum(MemberScope member) { + return this.getSchemaAnnotationValue(member, Schema::minimum, minimum -> !minimum.isEmpty()) + .filter(_minimum -> this.getSchemaAnnotationValue(member, Schema::exclusiveMinimum, Boolean.FALSE::equals).isPresent()) + .map(BigDecimal::new) + .orElse(null); + } + + /** + * Determine the given field/method's {@link Schema} annotation is present and contains a specific {@code minItems}. + * + * @param member potentially annotated field/method + * @return the {@code @ArraySchema(minItems)} value, otherwise {@code null} + */ + protected Integer resolveArrayMinItems(MemberScope member) { + if (member.isFakeContainerItemScope()) { + return null; + } + return this.getArraySchemaTypeAnnotation(member) + .map(Schema::minItems) + .filter(minItems -> minItems != Integer.MAX_VALUE) + .orElse(null); + } + + /** + * Determine the given field/method's {@link Schema} annotation is present and contains a specific {@code maxItems}. + * + * @param member potentially annotated field/method + * @return the {@code @ArraySchema(maxItems)} value, otherwise {@code null} + */ + protected Integer resolveArrayMaxItems(MemberScope member) { + if (member.isFakeContainerItemScope()) { + return null; + } + return this.getArraySchemaTypeAnnotation(member) + .map(Schema::maxItems) + .filter(maxItems -> maxItems != Integer.MIN_VALUE) + .orElse(null); + } + + /** + * Determine the given field/method's {@link Schema} annotation is present and is marked as {@code uniqueItems = true}. + * + * @param member potentially annotated field/method + * @return whether {@code @ArraySchema(uniqueItems = true)} is present + */ + protected Boolean resolveArrayUniqueItems(MemberScope member) { + if (member.isFakeContainerItemScope()) { + return null; + } + return this.getArraySchemaTypeAnnotation(member) + .map(Schema::uniqueItems) + .filter(uniqueItemsFlag -> uniqueItemsFlag) + .orElse(null); + } + + /** + * Implementation of the {@code CustomPropertyDefinitionProvider} to consider external references given in {@code @Schema(ref = ...)}. + * + * @param scope field/method to determine custom definition for + * @param context generation context + * @return custom definition containing the looked-up external reference or null + */ + protected CustomPropertyDefinition provideCustomSchemaDefinition(MemberScope scope, SchemaGenerationContext context) { + Optional externalReference = this.getSchemaAnnotationValue(scope, Schema::ref, ref -> !ref.isEmpty()); + if (!externalReference.isPresent()) { + return null; + } + // in Draft 6 and Draft 7, no other keywords are allowed besides a "$ref" + CustomDefinition.AttributeInclusion attributeInclusion; + switch (context.getGeneratorConfig().getSchemaVersion()) { + case DRAFT_6: + // fall-through (same as Draft 7) + case DRAFT_7: + attributeInclusion = CustomDefinition.AttributeInclusion.NO; + break; + default: + attributeInclusion = CustomDefinition.AttributeInclusion.YES; + } + ObjectNode reference = context.getGeneratorConfig().createObjectNode() + .put(context.getKeyword(SchemaKeyword.TAG_REF), externalReference.get()); + return new CustomPropertyDefinition(reference, attributeInclusion); + } + + /** + * Consider various remaining aspects. + *
    + *
  • {@code @Schema(not = ...)}
  • + *
  • {@code @Schema(allOf = ...)}
  • + *
  • {@code @Schema(minProperties = ...)}
  • + *
  • {@code @Schema(maxProperties = ...)}
  • + *
  • {@code @Schema(requiredProperties = ...)}
  • + *
+ * + * @param memberAttributes already collected schema for the field/method + * @param member targeted field/method + * @param context generation context + */ + protected void overrideInstanceAttributes(ObjectNode memberAttributes, MemberScope member, SchemaGenerationContext context) { + Schema annotation = this.getSchemaAnnotationValue(member, Function.identity(), x -> true) + .orElse(null); + if (annotation == null) { + return; + } + if (annotation.not() != Void.class) { + memberAttributes.set(context.getKeyword(SchemaKeyword.TAG_NOT), + context.createDefinitionReference(context.getTypeContext().resolve(annotation.not()))); + } + if (annotation.allOf().length > 0) { + ArrayNode allOfArray = memberAttributes.withArray(context.getKeyword(SchemaKeyword.TAG_ALLOF)); + Stream.of(annotation.allOf()) + .map(context.getTypeContext()::resolve) + .map(context::createDefinitionReference) + .forEach(allOfArray::add); + } + if (annotation.anyOf().length > 0) { + // since 4.26.0 + ArrayNode allOfArray = memberAttributes.withArray(context.getKeyword(SchemaKeyword.TAG_ALLOF)); + ArrayNode anyOfArray = allOfArray.addObject().withArray(context.getKeyword(SchemaKeyword.TAG_ANYOF)); + Stream.of(annotation.anyOf()) + .map(context.getTypeContext()::resolve) + .map(context::createDefinitionReference) + .forEach(anyOfArray::add); + } + if (annotation.oneOf().length > 0) { + // since 4.26.0 + ArrayNode allOfArray = memberAttributes.withArray(context.getKeyword(SchemaKeyword.TAG_ALLOF)); + ArrayNode oneOfArray = allOfArray.addObject().withArray(context.getKeyword(SchemaKeyword.TAG_ONEOF)); + Stream.of(annotation.oneOf()) + .map(context.getTypeContext()::resolve) + .map(context::createDefinitionReference) + .forEach(oneOfArray::add); + } + if (annotation.minProperties() > 0) { + memberAttributes.put(context.getKeyword(SchemaKeyword.TAG_PROPERTIES_MIN), annotation.minProperties()); + } + if (annotation.maxProperties() > 0) { + memberAttributes.put(context.getKeyword(SchemaKeyword.TAG_PROPERTIES_MAX), annotation.maxProperties()); + } + if (annotation.requiredProperties().length > 0) { + Set alreadyMentionedRequiredFields = new HashSet<>(); + ArrayNode requiredFieldNames = memberAttributes + .withArray(context.getKeyword(SchemaKeyword.TAG_REQUIRED)); + requiredFieldNames + .forEach(arrayItem -> alreadyMentionedRequiredFields.add(arrayItem.asText())); + Stream.of(annotation.requiredProperties()) + .filter(field -> !alreadyMentionedRequiredFields.contains(field)) + .forEach(requiredFieldNames::add); + } + } + + /** + * Look up a value from a {@link Schema} annotation on the given property or its associated field/getter or an external class referenced by + * {@link Schema#implementation()}. + * + * @param member field/method for which to look-up any present {@link Schema} annotation + * @param valueExtractor the getter for the value from the annotation + * @param valueFilter filter that determines whether the value from a given annotation matches our criteria + * @param the type of the returned value + * @return the value from one of the matching {@link Schema} annotations or {@code Optional.empty()} + */ + private Optional getSchemaAnnotationValue(MemberScope member, Function valueExtractor, Predicate valueFilter) { + return Optional.ofNullable(member.getAnnotationConsideringFieldAndGetter(Schema.class)) + .map(valueExtractor) + .filter(valueFilter); + } + + /** + * Create a {@link ConfigFunction} that extracts a value from the {@link Schema} annotation of a {@link TypeScope}. + * + * @param valueExtractor the getter for the value from the annotation + * @param valueFilter filter that determines whether the value from a given annotation matches our criteria + * @param the type of the returned value + * @return the value from the matching type's {@link Schema} annotation or {@code Optional.empty()} + */ + private ConfigFunction createTypePropertyResolver( + Function valueExtractor, Predicate valueFilter) { + return typeScope -> Optional + .ofNullable(typeScope.getType().getErasedType().getAnnotation(Schema.class)) + .map(valueExtractor).filter(valueFilter).orElse(null); + } + + /** + * Look-up the {@link Schema} annotation which is an array SchemaType on the given property or its associated field/getter. + * + * @param member field/method for which to look-up any present {@link Schema} annotation which is an array SchemaType + * @return present {@link Schema} annotation or {@code Optional.empty()} + */ + private Optional getArraySchemaTypeAnnotation(MemberScope member) { + return Optional.ofNullable(member.getAnnotationConsideringFieldAndGetter(Schema.class)) + .filter(schema -> SchemaType.ARRAY.equals(schema.type())); + } + +} diff --git a/jsonschema-module-microprofile-openapi-3/src/main/java/com/github/victools/jsonschema/module/microprofile/openapi3/MicroProfileOpenApi3SchemaDefinitionNamingStrategy.java b/jsonschema-module-microprofile-openapi-3/src/main/java/com/github/victools/jsonschema/module/microprofile/openapi3/MicroProfileOpenApi3SchemaDefinitionNamingStrategy.java new file mode 100644 index 00000000..2f60780c --- /dev/null +++ b/jsonschema-module-microprofile-openapi-3/src/main/java/com/github/victools/jsonschema/module/microprofile/openapi3/MicroProfileOpenApi3SchemaDefinitionNamingStrategy.java @@ -0,0 +1,64 @@ +/* + * Copyright 2024 VicTools. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.victools.jsonschema.module.microprofile.openapi3; + +import com.github.victools.jsonschema.generator.SchemaGenerationContext; +import com.github.victools.jsonschema.generator.impl.DefinitionKey; +import com.github.victools.jsonschema.generator.naming.DefaultSchemaDefinitionNamingStrategy; +import com.github.victools.jsonschema.generator.naming.SchemaDefinitionNamingStrategy; +import java.util.Map; +import org.eclipse.microprofile.openapi.annotations.media.Schema; + +/** + * Naming strategy for the keys in the {@code definitions}/{@code $defs} of the produced schema, based on {@code @Schema(name = ...)}. + */ +public class MicroProfileOpenApi3SchemaDefinitionNamingStrategy implements SchemaDefinitionNamingStrategy { + + private final SchemaDefinitionNamingStrategy baseStrategy; + + /** + * Constructor expecting a base strategy to be applied if there is no {@link Schema} annotation with a specific {@code name} being specified. + * + * @param baseStrategy fall-back strategy to be applied + */ + public MicroProfileOpenApi3SchemaDefinitionNamingStrategy(SchemaDefinitionNamingStrategy baseStrategy) { + if (baseStrategy == null) { + this.baseStrategy = new DefaultSchemaDefinitionNamingStrategy(); + } else { + this.baseStrategy = baseStrategy; + } + } + + @Override + public String getDefinitionNameForKey(DefinitionKey key, SchemaGenerationContext generationContext) { + Schema annotation = key.getType().getErasedType().getAnnotation(Schema.class); + if (annotation == null || annotation.name().isEmpty()) { + return this.baseStrategy.getDefinitionNameForKey(key, generationContext); + } + return annotation.name(); + } + + @Override + public void adjustDuplicateNames(Map subschemasWithDuplicateNames, SchemaGenerationContext generationContext) { + this.baseStrategy.adjustDuplicateNames(subschemasWithDuplicateNames, generationContext); + } + + @Override + public String adjustNullableName(DefinitionKey key, String definitionName, SchemaGenerationContext generationContext) { + return this.baseStrategy.adjustNullableName(key, definitionName, generationContext); + } +} diff --git a/jsonschema-module-microprofile-openapi-3/src/test/java/com/github/victools/jsonschema/module/microprofile/openapi3/IntegrationTest.java b/jsonschema-module-microprofile-openapi-3/src/test/java/com/github/victools/jsonschema/module/microprofile/openapi3/IntegrationTest.java new file mode 100644 index 00000000..03532aeb --- /dev/null +++ b/jsonschema-module-microprofile-openapi-3/src/test/java/com/github/victools/jsonschema/module/microprofile/openapi3/IntegrationTest.java @@ -0,0 +1,151 @@ +/* + * Copyright 2024 VicTools. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.victools.jsonschema.module.microprofile.openapi3; + +import com.fasterxml.jackson.databind.JsonNode; +import com.github.victools.jsonschema.generator.*; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Scanner; + +/** + * Integration test of this module being used in a real SchemaGenerator instance. + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class IntegrationTest { + + private SchemaGenerator generator; + + @BeforeAll + public void setUp() { + MicroProfileOpenApi3Module module = new MicroProfileOpenApi3Module(); + SchemaGeneratorConfigBuilder configBuilder = new SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON) + .with(Option.DEFINITIONS_FOR_ALL_OBJECTS, Option.NULLABLE_ARRAY_ITEMS_ALLOWED) + .with(Option.NONSTATIC_NONVOID_NONGETTER_METHODS, Option.FIELDS_DERIVED_FROM_ARGUMENTFREE_METHODS) + .with(module); + this.generator = new SchemaGenerator(configBuilder.build()); + } + + @ParameterizedTest + @ValueSource(classes = {TestClass.class, Foo.class}) + public void testIntegration(Class rawTargetType) throws Exception { + JsonNode result = this.generator.generateSchema(rawTargetType); + + String rawJsonSchema = result.toString(); + JSONAssert.assertEquals('\n' + rawJsonSchema + '\n', + loadResource("integration-test-result-" + rawTargetType.getSimpleName() + ".json"), rawJsonSchema, + JSONCompareMode.STRICT); + } + + private static String loadResource(String resourcePath) throws IOException { + StringBuilder stringBuilder = new StringBuilder(); + try (InputStream inputStream = IntegrationTest.class.getResourceAsStream(resourcePath)) { + assert inputStream != null; + try (Scanner scanner = new Scanner(inputStream, StandardCharsets.UTF_8.name())) { + while (scanner.hasNext()) { + stringBuilder.append(scanner.nextLine()).append('\n'); + } + } + } + return stringBuilder.toString(); + + } + + @Schema(minProperties = 2, maxProperties = 5, requiredProperties = {"fieldWithInclusiveNumericRange"}, ref = "./TestClass-schema.json") + static class TestClass { + + @Schema(hidden = true) + public Object hiddenField; + + @Schema(type = SchemaType.ARRAY, name = "fieldWithOverriddenName", defaultValue = "true", nullable = true, + minItems = 1, maxItems = 20, required = true) + public List originalFieldName; + + @Schema(description = "field description", nullable = true, enumeration = {"A", "B", "C", "D"}, minLength = 1, maxLength = 1) + public String fieldWithDescriptionAndAllowableValues; + + @Schema(minimum = "15", maximum = "20", multipleOf = 0.0123456789, required = false) + public BigDecimal fieldWithInclusiveNumericRange; + + @Schema(minimum = "14", maximum = "21", exclusiveMinimum = true, exclusiveMaximum = true, multipleOf = 0.1, + required = true) + public int fieldWithExclusiveNumericRange; + } + + @Schema(anyOf = {Reference.class, PersonReference.class}) + interface IReference { + + String getName(); + } + + static class Reference implements IReference { + + private String name; + + @Override + public String getName() { + return this.name; + } + } + + static class Person { + + } + + @Schema(description = "the foo's person", title = "reference title", name = "referenceToPerson") + static class PersonReference extends Reference { + + @Override + @Schema(description = "the person's name") + public String getName() { + return super.getName(); + } + } + + @Schema(additionalProperties = Schema.False.class) + static class Foo { + + @Schema(implementation = PersonReference.class, writeOnly = true) + private Reference person; + + @Schema(ref = "http://example.com/bar", readOnly = true, + additionalProperties = Schema.True.class) + private Object bar; + + @Schema(anyOf = {Double.class, Integer.class}) + private Object anyOfDoubleOrInt; + + @Schema(oneOf = {Boolean.class, String.class}) + private Object oneOfBooleanOrString; + + // on a member, the "additionalProperties" attribute is ignored + @Schema(additionalProperties = Schema.False.class) + private TestClass test; + } +} diff --git a/jsonschema-module-microprofile-openapi-3/src/test/java/com/github/victools/jsonschema/module/microprofile/openapi3/MicroProfileOpenApi3ModuleTest.java b/jsonschema-module-microprofile-openapi-3/src/test/java/com/github/victools/jsonschema/module/microprofile/openapi3/MicroProfileOpenApi3ModuleTest.java new file mode 100644 index 00000000..6a90420e --- /dev/null +++ b/jsonschema-module-microprofile-openapi-3/src/test/java/com/github/victools/jsonschema/module/microprofile/openapi3/MicroProfileOpenApi3ModuleTest.java @@ -0,0 +1,114 @@ +/* + * Copyright 2024 VicTools. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.victools.jsonschema.module.microprofile.openapi3; + +import com.github.victools.jsonschema.generator.*; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; + +import java.util.function.BiFunction; + +/** + * Test for the {@link MicroProfileOpenApi3Module} class. + */ +public class MicroProfileOpenApi3ModuleTest { + + @Mock + private SchemaGeneratorConfigBuilder configBuilder; + @Spy + private SchemaGeneratorGeneralConfigPart typesInGeneralConfigPart; + @Spy + private SchemaGeneratorConfigPart fieldConfigPart; + @Spy + private SchemaGeneratorConfigPart methodConfigPart; + + private AutoCloseable mockProvider; + + @BeforeEach + public void setUp() { + this.mockProvider = MockitoAnnotations.openMocks(this); + Mockito.when(this.configBuilder.forTypesInGeneral()).thenReturn(this.typesInGeneralConfigPart); + Mockito.when(this.configBuilder.forFields()).thenReturn(this.fieldConfigPart); + Mockito.when(this.configBuilder.forMethods()).thenReturn(this.methodConfigPart); + } + + @AfterEach + public void tearDown() throws Exception { + this.mockProvider.close(); + } + + @Test + public void testApplyToConfigBuilder() { + new MicroProfileOpenApi3Module().applyToConfigBuilder(this.configBuilder); + + Mockito.verify(this.configBuilder).forTypesInGeneral(); + Mockito.verify(this.configBuilder).forFields(); + Mockito.verify(this.configBuilder).forMethods(); + + Mockito.verify(this.typesInGeneralConfigPart).withDescriptionResolver(Mockito.any()); + Mockito.verify(this.typesInGeneralConfigPart).withTitleResolver(Mockito.any()); + Mockito.verify(this.typesInGeneralConfigPart).withAdditionalPropertiesResolver(Mockito.any(ConfigFunction.class)); + Mockito.verify(this.typesInGeneralConfigPart).withAdditionalPropertiesResolver(Mockito.any(BiFunction.class)); + Mockito.verify(this.typesInGeneralConfigPart).withCustomDefinitionProvider(Mockito.any(ExternalRefCustomDefinitionProvider.class)); + Mockito.verify(this.typesInGeneralConfigPart).withSubtypeResolver(Mockito.any(MicroProfileOpenApi3AnyOfResolver.class)); + Mockito.verify(this.typesInGeneralConfigPart).getDefinitionNamingStrategy(); + Mockito.verify(this.typesInGeneralConfigPart).withDefinitionNamingStrategy(Mockito.any(MicroProfileOpenApi3SchemaDefinitionNamingStrategy.class)); + + this.verifyCommonMemberConfigurations(this.fieldConfigPart); + this.verifyCommonMemberConfigurations(this.methodConfigPart); + + Mockito.verifyNoMoreInteractions(this.configBuilder, this.typesInGeneralConfigPart, this.fieldConfigPart, this.methodConfigPart); + } + + private void verifyCommonMemberConfigurations(SchemaGeneratorConfigPart configPart) { + Mockito.verify(configPart).withTargetTypeOverridesResolver(Mockito.any()); + Mockito.verify(configPart).withIgnoreCheck(Mockito.any()); + Mockito.verify(configPart).withPropertyNameOverrideResolver(Mockito.any()); + Mockito.verify(configPart).withCustomDefinitionProvider(Mockito.any()); + + Mockito.verify(configPart).withDescriptionResolver(Mockito.any()); + Mockito.verify(configPart).withTitleResolver(Mockito.any()); + Mockito.verify(configPart).withRequiredCheck(Mockito.any()); + Mockito.verify(configPart).withNullableCheck(Mockito.any()); + Mockito.verify(configPart).withReadOnlyCheck(Mockito.any()); + Mockito.verify(configPart).withWriteOnlyCheck(Mockito.any()); + Mockito.verify(configPart).withEnumResolver(Mockito.any()); + Mockito.verify(configPart).withDefaultResolver(Mockito.any()); + + Mockito.verify(configPart).withStringMinLengthResolver(Mockito.any()); + Mockito.verify(configPart).withStringMaxLengthResolver(Mockito.any()); + Mockito.verify(configPart).withStringFormatResolver(Mockito.any()); + Mockito.verify(configPart).withStringPatternResolver(Mockito.any()); + + Mockito.verify(configPart).withNumberMultipleOfResolver(Mockito.any()); + Mockito.verify(configPart).withNumberInclusiveMinimumResolver(Mockito.any()); + Mockito.verify(configPart).withNumberExclusiveMinimumResolver(Mockito.any()); + Mockito.verify(configPart).withNumberInclusiveMaximumResolver(Mockito.any()); + Mockito.verify(configPart).withNumberExclusiveMaximumResolver(Mockito.any()); + + Mockito.verify(configPart).withArrayMinItemsResolver(Mockito.any()); + Mockito.verify(configPart).withArrayMaxItemsResolver(Mockito.any()); + Mockito.verify(configPart).withArrayUniqueItemsResolver(Mockito.any()); + + Mockito.verify(configPart).withInstanceAttributeOverride(Mockito.any(InstanceAttributeOverrideV2.class)); + } +} diff --git a/jsonschema-module-microprofile-openapi-3/src/test/resources/com/github/victools/jsonschema/module/microprofile/openapi3/integration-test-result-Foo.json b/jsonschema-module-microprofile-openapi-3/src/test/resources/com/github/victools/jsonschema/module/microprofile/openapi3/integration-test-result-Foo.json new file mode 100644 index 00000000..cc2893cc --- /dev/null +++ b/jsonschema-module-microprofile-openapi-3/src/test/resources/com/github/victools/jsonschema/module/microprofile/openapi3/integration-test-result-Foo.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$defs": { + "referenceToPerson": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "the person's name" + } + }, + "title": "reference title", + "description": "the foo's person", + "additionalProperties": false + } + }, + "type": "object", + "properties": { + "anyOfDoubleOrInt": { + "anyOf": [{ + "type": "number" + }, { + "type": "integer" + }] + }, + "bar": { + "$ref": "http://example.com/bar", + "readOnly": true + }, + "oneOfBooleanOrString": { + "oneOf": [{ + "type": "boolean" + }, { + "type": "string" + }] + }, + "person": { + "$ref": "#/$defs/referenceToPerson", + "writeOnly": true + }, + "test": { + "$ref": "./TestClass-schema.json" + } + }, + "additionalProperties": false +} \ No newline at end of file diff --git a/jsonschema-module-microprofile-openapi-3/src/test/resources/com/github/victools/jsonschema/module/microprofile/openapi3/integration-test-result-TestClass.json b/jsonschema-module-microprofile-openapi-3/src/test/resources/com/github/victools/jsonschema/module/microprofile/openapi3/integration-test-result-TestClass.json new file mode 100644 index 00000000..fd2ec23e --- /dev/null +++ b/jsonschema-module-microprofile-openapi-3/src/test/resources/com/github/victools/jsonschema/module/microprofile/openapi3/integration-test-result-TestClass.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "object", + "properties": { + "fieldWithDescriptionAndAllowableValues": { + "type": ["string", "null"], + "description": "field description", + "enum": ["A", "B", "C", "D"], + "minLength": 1, + "maxLength": 1 + }, + "fieldWithExclusiveNumericRange": { + "type": "integer", + "exclusiveMinimum": 14, + "exclusiveMaximum": 21, + "multipleOf": 0.1 + }, + "fieldWithInclusiveNumericRange": { + "type": "number", + "minimum": 15, + "maximum": 20, + "multipleOf": 0.0123456789 + }, + "fieldWithOverriddenName": { + "minItems": 1, + "maxItems": 20, + "type": "array", + "items": { + "type": ["boolean", "null"], + "default": "true" + } + } + }, + "required": ["fieldWithExclusiveNumericRange", "fieldWithOverriddenName"] +} diff --git a/jsonschema-module-microprofile-openapi-3/src/test/resources/logback.xml b/jsonschema-module-microprofile-openapi-3/src/test/resources/logback.xml new file mode 100644 index 00000000..938180af --- /dev/null +++ b/jsonschema-module-microprofile-openapi-3/src/test/resources/logback.xml @@ -0,0 +1,12 @@ + + + + + %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n + + + + + + + diff --git a/pom.xml b/pom.xml index 115cdd78..bb45ce7c 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,7 @@ jsonschema-module-jackson jsonschema-module-jakarta-validation jsonschema-module-javax-validation + jsonschema-module-microprofile-openapi-3 jsonschema-module-swagger-1.5 jsonschema-module-swagger-2 jsonschema-maven-plugin @@ -40,6 +41,7 @@ jsonschema-module-jackson jsonschema-module-jakarta-validation jsonschema-module-javax-validation + jsonschema-module-microprofile-openapi-3 jsonschema-module-swagger-1.5 jsonschema-module-swagger-2 jsonschema-maven-plugin