diff --git a/api/pom.xml b/api/pom.xml index 763f5643..96fb2132 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -33,6 +33,12 @@ osgi.annotation provided + + biz.aQute.bnd + biz.aQute.bnd.annotation + 7.0.0 + provided + diff --git a/api/src/main/java/module-info.java b/api/src/main/java/module-info.java index cf6617b9..3ce0a2c6 100644 --- a/api/src/main/java/module-info.java +++ b/api/src/main/java/module-info.java @@ -52,5 +52,5 @@ // Required for compilation, not used at runtime requires static osgi.annotation; - + requires static biz.aQute.bnd.annotation; } diff --git a/api/src/main/java/org/eclipse/microprofile/openapi/models/Extensible.java b/api/src/main/java/org/eclipse/microprofile/openapi/models/Extensible.java index 90da485d..88589af0 100644 --- a/api/src/main/java/org/eclipse/microprofile/openapi/models/Extensible.java +++ b/api/src/main/java/org/eclipse/microprofile/openapi/models/Extensible.java @@ -23,19 +23,24 @@ * The base interface for OpenAPI model objects that can contain extensions. Extensions contain data not required by the * specification and may or may not be supported by the tools you use. *

- * The extensions property names are always prefixed by "x-". + * The extension's property names are always prefixed by "x-" unless otherwise specified by sub-interfaces or Extensible + * methods overridden therein. Sub-interfaces may also include additional detail regarding the handling of extension + * properties and how they relate to other well-known properties of the interface. + * + * For example, {@link org.eclipse.microprofile.openapi.models.media.Schema Schema} defines such handling and relaxes + * the requirement that extension properties are prefixed by "x-". */ public interface Extensible> { /** - * Returns the extensions property from an Extensible instance. + * Returns the map of all extension properties from an Extensible instance. * * @return a map containing keys which start with "x-" and values which provide additional information **/ Map getExtensions(); /** - * Sets this Extensible's extensions property to the given map of extensions. + * Sets this Extensible's extension properties to the given map of extensions. * * @param extensions * map containing keys which start with "x-" and values which provide additional information @@ -49,7 +54,7 @@ default T extensions(Map extensions) { } /** - * Adds the given object to this Extensible's map of extensions, with the given name as its key. + * Adds the given extension property to this Extensible, with the given name as its key. * * @param name * the key used to access the extension object. Always prefixed by "x-". @@ -61,7 +66,7 @@ default T extensions(Map extensions) { T addExtension(String name, Object value); /** - * Removes the given object to this Extensible's map of extensions, with the given name as its key. + * Removes an extension with the given property name from this Extensible. * * @param name * the key used to access the extension object. Always prefixed by "x-". @@ -69,7 +74,7 @@ default T extensions(Map extensions) { void removeExtension(String name); /** - * Sets this Extensible's extensions property to the given map of extensions. + * Sets this Extensible's extension properties to the given map of extensions. * * @param extensions * map containing keys which start with "x-" and values which provide additional information @@ -77,7 +82,7 @@ default T extensions(Map extensions) { void setExtensions(Map extensions); /** - * Checks whether an extension with the given name is present in this Extensible's map of extensions. + * Checks whether an extension with the given name is present in this Extensible. * * @param name * the key used to access the extension object. Always prefixed by "x-". @@ -93,7 +98,7 @@ default boolean hasExtension(String name) { } /** - * Returns the extension object with the given name from this Extensible's map of extensions. + * Returns the object with the given name from this Extensible's extensions. * * @param name * the key used to access the extension object. Always prefixed by "x-". diff --git a/api/src/main/java/org/eclipse/microprofile/openapi/models/media/Schema.java b/api/src/main/java/org/eclipse/microprofile/openapi/models/media/Schema.java index ed8daad1..1a6f6a06 100644 --- a/api/src/main/java/org/eclipse/microprofile/openapi/models/media/Schema.java +++ b/api/src/main/java/org/eclipse/microprofile/openapi/models/media/Schema.java @@ -39,6 +39,13 @@ * Any time a Schema Object can be used, a Reference Object can be used in its place. This allows referencing an * existing definition instead of defining the same Schema again. * + *

Extensions

Although Schema is {@link Extensible}, the behavior of extensions is only + * defined for the OAS schema dialect identified by URI {@code https://spec.openapis.org/oas/3.1/dialect/base} (the + * default if not specified). For this dialect, Schema instances will consider all unknown properties to be extensions. + * + * The behavior is undefined when adding an extension via one of the methods of the {@link Extensible} interface when + * the name of the extension matches the name of a standard OAS schema property. + * * @see OpenAPI Specification Schema Object */ public interface Schema extends Extensible, Constructible, Reference { @@ -2060,6 +2067,9 @@ default Schema examples(List examples) { * } * * + *

+ * Note, if the property is an extension, this method is equivalent to {@link #getExtension(String)}. + * * @param propertyName * the property name * @return the value of the named property, or {@code null} if a property with the given name is not set @@ -2090,7 +2100,8 @@ default Schema examples(List examples) { * * *

- * When using the standard schema dialect, values set by this method can be retrieved by other methods. E.g. + * When using the standard schema dialect, values set by this method can be retrieved by other methods provided the + * type is compatible. E.g. * *

      * {@code
@@ -2099,6 +2110,9 @@ default Schema examples(List examples) {
      * }
      * 
      *
+     * 

+ * Note, if the property is an extension, this method is equivalent to {@link #addExtension(String, Object)}. + * * @param propertyName * the property name * @param value @@ -2122,7 +2136,8 @@ default Schema examples(List examples) { /** * Sets all properties of a schema. *

- * Equivalent to clearing all properties and then setting each property with {@link #set(String, Object)}. + * Equivalent to clearing all properties, including extensions, and then setting each property with + * {@link #set(String, Object)}. * * @param allProperties * the properties to set. Each value in the map must be valid according to the rules in @@ -2130,4 +2145,95 @@ default Schema examples(List examples) { * @since 4.0 */ void setAll(Map allProperties); + + /** + * Returns the map of all extension properties of the schema. + * + * @return a map containing all of this schema's extension properties + * + * @see Schema Extensions + **/ + @Override + Map getExtensions(); + + /** + * Sets this schema's extension properties to the given map of extensions. Passing an empty map has the effect of + * clearing all existing extension properties. + * + * @param extensions + * map containing the extension properties for this schema + * @return the current instance + * + * @see Schema Extensions + */ + @Override + @aQute.bnd.annotation.baseline.BaselineIgnore("4.2.0") + default Schema extensions(Map extensions) { + return Extensible.super.extensions(extensions); + } + + /** + * Sets a schema extension property. + * + * @param name + * the extension property name + * @param value + * extension property value. null values will be rejected (implementation will throw an exception) or + * ignored. + * @return the current instance + * + * @see Schema Extensions + */ + @Override + @aQute.bnd.annotation.baseline.BaselineIgnore("4.2.0") + Schema addExtension(String name, Object value); + + /** + * Removes the given extension property from the schema. + * + * @param name + * the extension property name + * + * @see Schema Extensions + */ + @Override + void removeExtension(String name); + + /** + * Sets this schema's extension properties to the given map of extensions. Passing an empty map has the effect of + * clearing all existing extension properties. + * + * @param extensions + * map containing the extension properties for this schema + * + * @see Schema Extensions + */ + @Override + void setExtensions(Map extensions); + + /** + * Checks whether an extension property with the given name is present on this schema. + * + * @param name + * the extension property name + * + * @return {@code true} if an extension with the given name is present, otherwise {@code false} + */ + @Override + default boolean hasExtension(String name) { + return Extensible.super.hasExtension(name); + } + + /** + * Returns the extension object with the given name from this schema. + * + * @param name + * the extension property name + * + * @return the corresponding extension object, or {@code null} if no extension with the given name is present + */ + @Override + default Object getExtension(String name) { + return Extensible.super.getExtension(name); + } } diff --git a/api/src/main/java/org/eclipse/microprofile/openapi/models/media/package-info.java b/api/src/main/java/org/eclipse/microprofile/openapi/models/media/package-info.java index a1fb925b..0de474e2 100644 --- a/api/src/main/java/org/eclipse/microprofile/openapi/models/media/package-info.java +++ b/api/src/main/java/org/eclipse/microprofile/openapi/models/media/package-info.java @@ -30,6 +30,6 @@ * */ -@org.osgi.annotation.versioning.Version("3.1") +@org.osgi.annotation.versioning.Version("3.2") @org.osgi.annotation.versioning.ProviderType package org.eclipse.microprofile.openapi.models.media; \ No newline at end of file diff --git a/spec/src/main/asciidoc/release_notes.asciidoc b/spec/src/main/asciidoc/release_notes.asciidoc index 22419566..ac91c486 100644 --- a/spec/src/main/asciidoc/release_notes.asciidoc +++ b/spec/src/main/asciidoc/release_notes.asciidoc @@ -28,6 +28,7 @@ A full list of changes delivered in the 4.2 release can be found at link:https:/ ==== API/SPI changes * Add `example` and `examples` to `@Header` and verify implementation support in TCK (https://github.com/microprofile/microprofile-open-api/issues/697)[697]) +* Override `@Extensible`'s methods in `@Schema`, providing clarification in the documentation on how the methods behave specifically for schemas (https://github.com/microprofile/microprofile-open-api/issues/698[698]) [[other_changes_42]] ==== Other Changes diff --git a/tck/src/main/java/org/eclipse/microprofile/openapi/tck/SchemaExtensionPropertyTest.java b/tck/src/main/java/org/eclipse/microprofile/openapi/tck/SchemaExtensionPropertyTest.java new file mode 100644 index 00000000..f020df07 --- /dev/null +++ b/tck/src/main/java/org/eclipse/microprofile/openapi/tck/SchemaExtensionPropertyTest.java @@ -0,0 +1,104 @@ +/** + * Copyright (c) 2026 Contributors to the Eclipse Foundation + *

+ * 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 org.eclipse.microprofile.openapi.tck; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.sameInstance; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.eclipse.microprofile.openapi.OASFactory; +import org.eclipse.microprofile.openapi.models.media.Schema; +import org.eclipse.microprofile.openapi.models.media.Schema.SchemaType; +import org.testng.annotations.Test; + +/** + * This test covers the Extensible aspect of the Schema model, checking that properties are recognized correctly as + * extensions by an implementation and handled according to the Schema documentation. + */ +public class SchemaExtensionPropertyTest { + + private static final String EXTNAME = "my-extension"; + + @Test + public void testExtensionSetForUnknownProperty() { + Schema schema = OASFactory.createSchema(); + Object theExtension = new Object(); + schema.set(EXTNAME, theExtension); + + assertThat(schema.get(EXTNAME), is(sameInstance(theExtension))); + assertThat(schema.getExtension(EXTNAME), is(sameInstance(theExtension))); + } + + @Test + public void testExtensionSetAllForUnknownProperty() { + Schema schema = OASFactory.createSchema(); + Object theExtension = new Object(); + schema.setAll(Map.of( + "type", List.of(SchemaType.STRING), + EXTNAME, theExtension)); + + assertThat(schema.getType(), is(List.of(SchemaType.STRING))); + assertThat(schema.get(EXTNAME), is(sameInstance(theExtension))); + assertThat(schema.getExtension(EXTNAME), is(sameInstance(theExtension))); + } + + @Test + public void testExtensionAvailableFromGet() { + Schema schema = OASFactory.createSchema(); + Object theExtension = new Object(); + schema.addExtension(EXTNAME, theExtension); + + assertThat(schema.get(EXTNAME), is(sameInstance(theExtension))); + assertThat(schema.getExtension(EXTNAME), is(sameInstance(theExtension))); + } + + @Test + public void testExtensionSetWithNonnullDialect() { + Schema schema = OASFactory.createSchema(); + schema.setSchemaDialect("https://spec.openapis.org/oas/3.1/dialect/base"); + Object theExtension = new Object(); + schema.set(EXTNAME, theExtension); + assertThat(schema.getExtension(EXTNAME), is(sameInstance(theExtension))); + } + + @Test + public void testSetAllClearsExtensions() { + Schema schema = OASFactory.createSchema(); + Object theExtension = new Object(); + schema.set(EXTNAME, theExtension); + assertThat(schema.getExtension(EXTNAME), is(sameInstance(theExtension))); + schema.setAll(Collections.emptyMap()); + assertThat(schema.hasExtension(EXTNAME), is(false)); + } + + @Test + public void testNullExtensionNotAdded() { + Schema schema = OASFactory.createSchema(); + + try { + schema.addExtension(EXTNAME, null); + } catch (Exception e) { + // May or may not be thrown + } + + assertThat(schema.hasExtension(EXTNAME), is(false)); + } +}