diff --git a/pom.xml b/pom.xml index a5ad6e9..99a162e 100644 --- a/pom.xml +++ b/pom.xml @@ -25,9 +25,9 @@ com.amartus.sonata blender SonataBlendingTool - 1.9.3 + 1.9.4-SNAPSHOT - 6.0.1 + 6.2.0 2.14.0 @@ -51,7 +51,7 @@ com.github.rvesse airline - 2.8.5 + 2.9.0 ch.qos.logback diff --git a/src/main/java/com/amartus/sonata/blender/impl/postprocess/ComposedPostprocessor.java b/src/main/java/com/amartus/sonata/blender/impl/postprocess/ComposedPostprocessor.java index cd9a1f5..4f92632 100644 --- a/src/main/java/com/amartus/sonata/blender/impl/postprocess/ComposedPostprocessor.java +++ b/src/main/java/com/amartus/sonata/blender/impl/postprocess/ComposedPostprocessor.java @@ -18,15 +18,27 @@ package com.amartus.sonata.blender.impl.postprocess; +import com.amartus.sonata.blender.impl.specifications.FragmentBasedNamingStrategy; +import com.amartus.sonata.blender.impl.specifications.PathBaseNamingStrategy; +import com.amartus.sonata.blender.impl.specifications.ProductSpecificationNamingStrategy; +import com.amartus.sonata.blender.impl.specifications.UrnBasedNamingStrategy; import io.swagger.v3.oas.models.OpenAPI; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.List; import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static com.amartus.sonata.blender.impl.postprocess.RenameTypesPostprocessor.*; public class ComposedPostprocessor implements Consumer { + private static final Logger log = LoggerFactory.getLogger(ComposedPostprocessor.class); private List> postprocessors = List.of( new RemoveSuperflousTypeDeclarations(), + new RenameTypesPostprocessor(converter()), new PropertyEnumExternalize(), new ComposedPropertyToType(), new SingleEnumToDiscriminatorValue(), @@ -34,12 +46,23 @@ public class ComposedPostprocessor implements Consumer { new UpdateDiscriminatorMapping(), new ConstrainDiscriminatorValueWithEnum() ); - - @Override public void accept(OpenAPI openAPI) { + log.info("Running {} OAS post-processors", postprocessors.size()); for (var p : postprocessors) { + log.debug("Running {}", p.getClass().getSimpleName()); p.accept(openAPI); } } + private NameConverter converter() { + var converters= Stream.of( + new UrnBasedNamingStrategy(), + new FragmentBasedNamingStrategy(), + new PathBaseNamingStrategy() + ).collect(Collectors.toList()); + + return input -> converters.stream().flatMap(it -> it.fromText(input).stream() + .map(ProductSpecificationNamingStrategy.NameAndDiscriminator::getName)) + .findFirst().orElse(null); + } } diff --git a/src/main/java/com/amartus/sonata/blender/impl/postprocess/ComposedPropertyToType.java b/src/main/java/com/amartus/sonata/blender/impl/postprocess/ComposedPropertyToType.java index 769c1ec..cf79a6b 100644 --- a/src/main/java/com/amartus/sonata/blender/impl/postprocess/ComposedPropertyToType.java +++ b/src/main/java/com/amartus/sonata/blender/impl/postprocess/ComposedPropertyToType.java @@ -72,7 +72,7 @@ protected Map.Entry processProperty(String name, Schema property if (property instanceof ArraySchema) { var s = ((ArraySchema) property).getItems(); var converted = convertProperty(name, s); - ((ArraySchema) property).setItems(converted.getValue()); + property.setItems(converted.getValue()); return Map.entry(converted.getKey(), property); } diff --git a/src/main/java/com/amartus/sonata/blender/impl/postprocess/ConvertOneOfToAllOffInheritance.java b/src/main/java/com/amartus/sonata/blender/impl/postprocess/ConvertOneOfToAllOffInheritance.java index baf5a7e..98a0c0e 100644 --- a/src/main/java/com/amartus/sonata/blender/impl/postprocess/ConvertOneOfToAllOffInheritance.java +++ b/src/main/java/com/amartus/sonata/blender/impl/postprocess/ConvertOneOfToAllOffInheritance.java @@ -17,6 +17,7 @@ */ package com.amartus.sonata.blender.impl.postprocess; +import com.amartus.sonata.blender.impl.util.Collections; import com.amartus.sonata.blender.impl.util.OasUtils; import com.amartus.sonata.blender.impl.util.OasWrapper; import io.swagger.v3.oas.models.OpenAPI; @@ -131,7 +132,7 @@ private Map> prepare() { .map(this::convertToOneOf) .filter(e -> e.getValue().stream().allMatch(isReference)) .map(e -> convertValues(e, v -> OasUtils.toSchemaName(v.get$ref()))) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + .collect(Collections.mapCollector()); } private Map.Entry> convertToOneOf(Map.Entry e) { diff --git a/src/main/java/com/amartus/sonata/blender/impl/postprocess/PropertyPostProcessor.java b/src/main/java/com/amartus/sonata/blender/impl/postprocess/PropertyPostProcessor.java index 5153f60..22f6815 100644 --- a/src/main/java/com/amartus/sonata/blender/impl/postprocess/PropertyPostProcessor.java +++ b/src/main/java/com/amartus/sonata/blender/impl/postprocess/PropertyPostProcessor.java @@ -47,6 +47,7 @@ protected void process(String type, Schema schema) { Map properties = toProperties(schema) .entrySet().stream() .map(e -> processProperty(e.getKey(), e.getValue())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); schema.setProperties(properties); if (schema instanceof ComposedSchema) { diff --git a/src/main/java/com/amartus/sonata/blender/impl/postprocess/RenameTypesPostprocessor.java b/src/main/java/com/amartus/sonata/blender/impl/postprocess/RenameTypesPostprocessor.java new file mode 100644 index 0000000..7be878b --- /dev/null +++ b/src/main/java/com/amartus/sonata/blender/impl/postprocess/RenameTypesPostprocessor.java @@ -0,0 +1,154 @@ +package com.amartus.sonata.blender.impl.postprocess; + +import com.amartus.sonata.blender.impl.util.OasUtils; +import com.amartus.sonata.blender.impl.util.OasWrapper; +import com.amartus.sonata.blender.impl.util.Pair; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.Content; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.parameters.RequestBody; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.oas.models.responses.ApiResponses; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.*; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class RenameTypesPostprocessor implements Consumer { + private static final Logger log = LoggerFactory.getLogger(RenameTypesPostprocessor.class); + public interface NameConverter { + String convert(String input); + } + private Map substitutions; + private final NameConverter converter; + public RenameTypesPostprocessor(NameConverter converter) { + this.converter = converter; + } + + private static final String extensionName = "x-try-renaming-on"; + + @Override + public void accept(OpenAPI openAPI) { + + var schemas = new OasWrapper(openAPI).schemas(); + this.substitutions = schemas.entrySet().stream() + .map(e -> Map.entry(e.getKey(), toName(e.getValue()).orElse(e.getKey()))) + .filter(p -> ! p.getKey().equals(p.getValue())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + if(substitutions.isEmpty()) return; + + if(log.isDebugEnabled()) { + substitutions.forEach((a,b) -> log.debug("Renaming {} to {}", a, b)); + } + + var newNames = new HashSet(substitutions.values()); + if(newNames.size() < substitutions.size()) { + log.warn("Conflicting substitution names. Skipping."); + return; + } + if(schemas.keySet().stream().anyMatch(newNames::contains)) { + log.warn("Name substitution already defined in the model. Skipping."); + return; + } + schemas.values().forEach(s -> Optional.ofNullable(s.getExtensions()).ifPresent(e -> e.remove(extensionName))); + renameSchemas(openAPI); + renameReferences(openAPI); + } + + private void renameSchemas(OpenAPI oas) { + var schemas = new OasWrapper(oas).schemas().entrySet().stream() + .map(e -> Pair.of(substitutions.getOrDefault(e.getKey(), e.getKey()), e.getValue())) + .collect(Collectors.toMap(Pair::first, Pair::second)); + oas.getComponents().setSchemas(schemas); + } + + protected Schema tryConverting(Schema schema) { + if(schema instanceof ArraySchema) { + var items = tryConverting(schema.getItems()); + schema.setItems(items); + } + schema.set$ref(convert(schema.get$ref())); + return schema; + } + + protected String convert(String ref) { + return Optional.ofNullable(ref) + .flatMap(r -> { + var name = OasUtils.toSchemaName(ref); + return Optional.ofNullable(substitutions.get(name)) + .map(OasUtils::toSchemRef); + }).orElse(ref); + } + + private void renameReferences(OpenAPI openAPI) { + new RenameReferences().accept(openAPI); + new RenamePathReferences().accept(openAPI); + //FIXME rename refrences in response ref and body ref + } + + protected Optional toName(Schema schema) { + var name = Optional.ofNullable(schema.getExtensions()) + .flatMap(e -> Optional.ofNullable(e.get(extensionName)).map(it -> (String)it)); + return name.flatMap(n -> Optional.ofNullable(converter.convert(n))); + } + + private class RenameReferences extends PropertyPostProcessor { + protected Map.Entry processProperty(String name, Schema property) { + return Map.entry(name, tryConverting(property)); + } + } + + private class RenamePathReferences implements Consumer { + + @Override + public void accept(OpenAPI openAPI) { + Stream schemas = toOperations(openAPI).flatMap(o -> { + var bs = schemas(o.getRequestBody()); + var rs = schemas(o.getResponses()); + return Stream.concat(bs, rs); + }); + + schemas.forEach(RenameTypesPostprocessor.this::tryConverting); + + openAPI.getPaths().entrySet().stream().map(Map.Entry::getValue) + .flatMap(pi -> pi.readOperations().stream()); + + } + + private Stream toOperations(OpenAPI oas) { + return Optional.ofNullable(oas.getPaths()) + .map(p -> p.entrySet().stream() + .map(Map.Entry::getValue).flatMap(pi -> pi.readOperations().stream()) + ).orElse(Stream.empty()); + } + + private Stream schemas(ApiResponses resp) { + return Optional.ofNullable(resp) + .map(LinkedHashMap::values).stream() + .flatMap(Collection::stream) + .flatMap(this::schemas); + } + private Stream schemas(ApiResponse r) { + if(r.get$ref() != null) return Stream.empty(); + return schemas(r.getContent()); + } + + private Stream schemas(Content c) { + if(c == null) Stream.empty(); + return c.values().stream().map(MediaType::getSchema); + } + + private Stream schemas(RequestBody rb) { + return Optional.ofNullable(rb).stream().flatMap(r -> { + if(r.get$ref() != null) return Stream.empty(); + return schemas(r.getContent()); + }); + } + } +} diff --git a/src/main/java/com/amartus/sonata/blender/impl/specifications/FragmentBasedNamingStrategy.java b/src/main/java/com/amartus/sonata/blender/impl/specifications/FragmentBasedNamingStrategy.java index 36386fc..7c0d279 100644 --- a/src/main/java/com/amartus/sonata/blender/impl/specifications/FragmentBasedNamingStrategy.java +++ b/src/main/java/com/amartus/sonata/blender/impl/specifications/FragmentBasedNamingStrategy.java @@ -28,15 +28,11 @@ public Optional provideNameAndDiscriminator(URI schemaLoca if (schemaLocation == null) { return Optional.empty(); } - return Optional.ofNullable(schemaLocation.getFragment()) - .map(f -> { - var idx = f.lastIndexOf("/"); - if (idx < 0 || idx == f.length() - 1) { - //TODO rethink - return f; - } - return f.substring(idx + 1); - }) - .map(n -> new NameAndDiscriminator(n, null)); + return fromText(schemaLocation.getFragment()); + } + + @Override + public Optional fromText(String fragment) { + return Optional.ofNullable(NameConverter.lastSegment.apply(fragment)); } } diff --git a/src/main/java/com/amartus/sonata/blender/impl/specifications/NameConverter.java b/src/main/java/com/amartus/sonata/blender/impl/specifications/NameConverter.java new file mode 100644 index 0000000..2478142 --- /dev/null +++ b/src/main/java/com/amartus/sonata/blender/impl/specifications/NameConverter.java @@ -0,0 +1,48 @@ +package com.amartus.sonata.blender.impl.specifications; + +import org.apache.commons.lang3.text.WordUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URI; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static com.amartus.sonata.blender.impl.util.TextUtils.split; + +public interface NameConverter extends Function { + + Logger log = LoggerFactory.getLogger(UrnBasedNamingStrategy.class); + + private static String toName(String type) { + return split(type, '-').map(WordUtils::capitalize) + .collect(Collectors.joining("")); + }; + + NameConverter urn = id -> { + var uri = URI.create(id); + + if ("urn".equals(uri.getScheme())) { + String[] segments = uri.getRawSchemeSpecificPart().split(":"); + if ("mef".equals(segments[0]) && segments.length == 7) { + try { + var name = toName(segments[4]); + return new ProductSpecificationNamingStrategy.NameAndDiscriminator(name, id); + } catch (NullPointerException | IndexOutOfBoundsException e) { + log.info("{} is not MEF urn", id); + } + } + } + return null; + }; + + private static ProductSpecificationNamingStrategy.NameAndDiscriminator lastSegment(String s) { + return Optional.ofNullable(s) + .flatMap(p -> split(p, '/').reduce((acc,b) -> b)) + .map(ProductSpecificationNamingStrategy.NameAndDiscriminator::new) + .orElse(null); + } + + NameConverter lastSegment = NameConverter::lastSegment; +} diff --git a/src/main/java/com/amartus/sonata/blender/impl/specifications/PathBaseNamingStrategy.java b/src/main/java/com/amartus/sonata/blender/impl/specifications/PathBaseNamingStrategy.java index f9bb96e..f6b32db 100644 --- a/src/main/java/com/amartus/sonata/blender/impl/specifications/PathBaseNamingStrategy.java +++ b/src/main/java/com/amartus/sonata/blender/impl/specifications/PathBaseNamingStrategy.java @@ -28,15 +28,11 @@ public Optional provideNameAndDiscriminator(URI schemaLoca if (schemaLocation == null) { return Optional.empty(); } - return Optional.ofNullable(schemaLocation.getPath()) - .map(f -> { - var idx = f.lastIndexOf("/"); - if (idx < 0 || idx == f.length() - 1) { - //TODO rethink - return f; - } - return f.substring(idx + 1); - }) - .map(n -> new NameAndDiscriminator(n, null)); + return fromText(schemaLocation.getPath()); + } + + @Override + public Optional fromText(String path) { + return Optional.ofNullable(NameConverter.lastSegment.apply(path)); } } diff --git a/src/main/java/com/amartus/sonata/blender/impl/specifications/ProductSpecificationNamingStrategy.java b/src/main/java/com/amartus/sonata/blender/impl/specifications/ProductSpecificationNamingStrategy.java index 626031e..a65441a 100644 --- a/src/main/java/com/amartus/sonata/blender/impl/specifications/ProductSpecificationNamingStrategy.java +++ b/src/main/java/com/amartus/sonata/blender/impl/specifications/ProductSpecificationNamingStrategy.java @@ -28,6 +28,7 @@ */ public interface ProductSpecificationNamingStrategy { Optional provideNameAndDiscriminator(URI schemaLocation, JsonNode fileContent); + Optional fromText(String id); class NameAndDiscriminator { private final String name; diff --git a/src/main/java/com/amartus/sonata/blender/impl/specifications/UrnBasedNamingStrategy.java b/src/main/java/com/amartus/sonata/blender/impl/specifications/UrnBasedNamingStrategy.java index b57f6e5..f5d0d9f 100644 --- a/src/main/java/com/amartus/sonata/blender/impl/specifications/UrnBasedNamingStrategy.java +++ b/src/main/java/com/amartus/sonata/blender/impl/specifications/UrnBasedNamingStrategy.java @@ -29,43 +29,17 @@ import java.util.stream.Stream; public class UrnBasedNamingStrategy implements ProductSpecificationNamingStrategy { - private static final Logger log = LoggerFactory.getLogger(UrnBasedNamingStrategy.class); - @Override public Optional provideNameAndDiscriminator(URI schemaLocation, JsonNode fileContent) { return Optional.ofNullable(fileContent) .map(fc -> fc.get("$id")) .flatMap(i -> Optional.ofNullable(i.textValue())) - .flatMap(this::convert); - } - - private Optional convert(String id) { - var uri = URI.create(id); - - if ("urn".equals(uri.getScheme())) { - String[] segments = uri.getRawSchemeSpecificPart().split(":"); - if ("mef".equals(segments[0]) && segments.length == 7) { - try { - var name = toName(segments[4]); - return Optional.of(new NameAndDiscriminator(name, id)); - } catch (NullPointerException | IndexOutOfBoundsException e) { - log.info("{} is not MEF urn", id); - } - } - - } - return Optional.empty(); - } - - private String toName(String type) { - return split(type, '-').map(WordUtils::capitalize) - .collect(Collectors.joining("")); + .flatMap(this::fromText); } - private Stream split(String word, char... separators) { - if (separators.length == 0) return Stream.of(word); - String regex = "[" + new String(separators) + "]"; - return Arrays.stream(word.split(regex)); + @Override + public Optional fromText(String id) { + return Optional.ofNullable(NameConverter.urn.apply(id)); } } diff --git a/src/main/java/com/amartus/sonata/blender/impl/util/Collections.java b/src/main/java/com/amartus/sonata/blender/impl/util/Collections.java new file mode 100644 index 0000000..97e74e8 --- /dev/null +++ b/src/main/java/com/amartus/sonata/blender/impl/util/Collections.java @@ -0,0 +1,17 @@ +package com.amartus.sonata.blender.impl.util; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.BinaryOperator; +import java.util.stream.Collector; +import java.util.stream.Collectors; + +public interface Collections { + static Collector, ?, LinkedHashMap> mapCollector() { + return mapCollector((a,b) -> a); + } + + static Collector, ?, LinkedHashMap> mapCollector(BinaryOperator merger) { + return Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, merger, LinkedHashMap::new); + } +} diff --git a/src/main/java/com/amartus/sonata/blender/impl/util/OasUtils.java b/src/main/java/com/amartus/sonata/blender/impl/util/OasUtils.java index 866bb1d..24567a1 100644 --- a/src/main/java/com/amartus/sonata/blender/impl/util/OasUtils.java +++ b/src/main/java/com/amartus/sonata/blender/impl/util/OasUtils.java @@ -87,11 +87,11 @@ static boolean isReferencingSchema(Schema schema) { return false; } - static long countReferences(Schema schema) { - final var counter = Helpers.safeConvert.andThen(Helpers.references); + static long countReferences(Schema schema) { if (schema instanceof ObjectSchema) { return Helpers.references.apply(Stream.of(schema)); } + final var counter = Helpers.safeConvert.andThen(Helpers.references); if (schema instanceof ComposedSchema) { var cs = (ComposedSchema) schema; return Stream.of( diff --git a/src/main/java/com/amartus/sonata/blender/impl/util/TextUtils.java b/src/main/java/com/amartus/sonata/blender/impl/util/TextUtils.java index eb086da..dd3f69f 100644 --- a/src/main/java/com/amartus/sonata/blender/impl/util/TextUtils.java +++ b/src/main/java/com/amartus/sonata/blender/impl/util/TextUtils.java @@ -19,9 +19,11 @@ package com.amartus.sonata.blender.impl.util; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Stream; public interface TextUtils { static List splitPhrases(String subject) { @@ -42,4 +44,10 @@ static List splitPhrases(String subject) { } return matchList; } + + static Stream split(String word, char... separators) { + if (separators.length == 0) return Stream.of(word); + String regex = "[" + new String(separators) + "]"; + return Arrays.stream(word.split(regex)); + } } diff --git a/src/main/java/com/amartus/sonata/blender/parser/DeserializerProvider.java b/src/main/java/com/amartus/sonata/blender/parser/DeserializerProvider.java index d1f449e..05d826c 100644 --- a/src/main/java/com/amartus/sonata/blender/parser/DeserializerProvider.java +++ b/src/main/java/com/amartus/sonata/blender/parser/DeserializerProvider.java @@ -12,19 +12,22 @@ public class DeserializerProvider { private static final Logger log = LoggerFactory.getLogger(DeserializerProvider.class); static class AmartusDeserializer extends OpenAPIDeserializer { + private Optional getByName(ObjectNode node, String name) { + return Optional.ofNullable(node.get(name)) + .map(JsonNode::textValue); + } @Override public Schema getSchema(ObjectNode node, String location, ParseResult result) { var schema = super.getSchema(node, location, result); if (schema.get$ref() != null) { - var description = Optional.ofNullable(node.get("description")) - .map(JsonNode::textValue); - description.ifPresent(d -> { + getByName(node, "description").ifPresent(d -> { log.debug("Adding description of a $ref property {}", schema.get$ref()); schema.setDescription(d); }); } + getByName(node,"$id").ifPresent(id -> schema.addExtension("x-try-renaming-on", id)); return schema; } } diff --git a/src/test/resources/ref-model/common/common.json b/src/test/resources/ref-model/common/common.json index 6606245..cdc329f 100644 --- a/src/test/resources/ref-model/common/common.json +++ b/src/test/resources/ref-model/common/common.json @@ -4,6 +4,12 @@ "properties": { "c1": { "type": "string" + }, + "a1": { + "type": "string" + }, + "d1" : { + "type": "integer" } } }