openapi-processor / openapi-processor-spring

an OpenAPI 3.0 & 3.1 interface & model java code generator for Spring Boot
https://docs.openapiprocessor.io
Apache License 2.0
40 stars 9 forks source link

Support complex generic type response mapping procesing #150

Open MetallFoX opened 1 year ago

MetallFoX commented 1 year ago

Hi @hauner!

Processing response mapping of generic types with depth 1 works fine:

FooToBar => java.util.Map<java.lang.String, java.lang.String>
FooToBar => java.util.Map<java.lang.String,  java.lang.List>

But it doesn't work with a complex generic types such as:

FooToBar => java.util.Map<java.lang.String, java.lang.List<java.lang.String>>

The output of processing:

line 1:64 no viable alternative at input 'java.util.Map<java.lang.String,java.lang.List<'
processing failed!
io.openapiprocessor.core.processor.mapping.v2.parser.antlr.MappingException: failed to parse mapping: FooToBar => java.util.Map<java.lang.String, java.lang.List<java.lang.String>>
    at io.openapiprocessor.core.processor.mapping.v2.parser.antlr.ParserKt.parseMapping(Parser.kt:27)
    at io.openapiprocessor.core.processor.mapping.v2.MappingConverter.convertType(MappingConverter.kt:91)
    at io.openapiprocessor.core.processor.mapping.v2.MappingConverter.convert(MappingConverter.kt:44)
    at io.openapiprocessor.core.processor.MappingConverter.convert(MappingConverter.kt:29)
    at io.openapiprocessor.core.converter.OptionsConverter.readMapping(OptionsConverter.kt:69)
    at io.openapiprocessor.core.converter.OptionsConverter.convertOptions(OptionsConverter.kt:34)
    at io.openapiprocessor.spring.processor.SpringProcessor.convertOptions(SpringProcessor.kt:116)
    at io.openapiprocessor.spring.processor.SpringProcessor.run(SpringProcessor.kt:45)
    at io.openapiprocessor.gradle.OpenApiProcessorWorker.run(OpenApiProcessorWorker.java:36)
    at io.openapiprocessor.gradle.OpenApiProcessorWorker.execute(OpenApiProcessorWorker.java:25)
    at org.gradle.workers.internal.DefaultWorkerServer.execute(DefaultWorkerServer.java:63)
    at org.gradle.workers.internal.AbstractClassLoaderWorker$1.create(AbstractClassLoaderWorker.java:49)
    at org.gradle.workers.internal.AbstractClassLoaderWorker$1.create(AbstractClassLoaderWorker.java:43)
    at org.gradle.internal.classloader.ClassLoaderUtils.executeInClassloader(ClassLoaderUtils.java:97)
    at org.gradle.workers.internal.AbstractClassLoaderWorker.executeInClassLoader(AbstractClassLoaderWorker.java:43)
    at org.gradle.workers.internal.IsolatedClassloaderWorker.run(IsolatedClassloaderWorker.java:49)
    at org.gradle.workers.internal.IsolatedClassloaderWorker.run(IsolatedClassloaderWorker.java:30)
    at org.gradle.workers.internal.IsolatedClassloaderWorkerFactory$1.lambda$execute$0(IsolatedClassloaderWorkerFactory.java:57)
    at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:44)
    at org.gradle.workers.internal.AbstractWorker$1.call(AbstractWorker.java:41)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:199)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)
    at org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:157)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)
    at org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)
    at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:73)
    at org.gradle.workers.internal.AbstractWorker.executeWrappedInBuildOperation(AbstractWorker.java:41)
    at org.gradle.workers.internal.IsolatedClassloaderWorkerFactory$1.execute(IsolatedClassloaderWorkerFactory.java:49)
    at org.gradle.workers.internal.DefaultWorkerExecutor.lambda$submitWork$2(DefaultWorkerExecutor.java:205)
    at java.base@18.0.1.1/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runExecution(DefaultConditionalExecutionQueue.java:187)
    at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.access$700(DefaultConditionalExecutionQueue.java:120)
    at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner$1.run(DefaultConditionalExecutionQueue.java:162)
    at org.gradle.internal.Factories$1.create(Factories.java:31)
    at org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:270)
    at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:119)
    at org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:124)
    at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.runBatch(DefaultConditionalExecutionQueue.java:157)
    at org.gradle.internal.work.DefaultConditionalExecutionQueue$ExecutionRunner.run(DefaultConditionalExecutionQueue.java:126)
    at java.base@18.0.1.1/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:539)
    at java.base@18.0.1.1/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
    at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48)
    at java.base@18.0.1.1/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
    at java.base@18.0.1.1/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
    at java.base@18.0.1.1/java.lang.Thread.run(Thread.java:833)
Caused by: io.openapiprocessor.core.processor.mapping.v2.parser.antlr.MappingParserException: no viable alternative at input 'java.util.Map<java.lang.String,java.lang.List<'
    at io.openapiprocessor.core.processor.mapping.v2.parser.antlr.MappingErrorListener.syntaxError(MappingErrorListener.kt:22)
    at org.antlr.v4.runtime.ProxyErrorListener.syntaxError(ProxyErrorListener.java:41)
    at org.antlr.v4.runtime.Parser.notifyErrorListeners(Parser.java:544)
    at org.antlr.v4.runtime.DefaultErrorStrategy.reportNoViableAlternative(DefaultErrorStrategy.java:310)
    at org.antlr.v4.runtime.DefaultErrorStrategy.reportError(DefaultErrorStrategy.java:136)
    at io.openapiprocessor.core.processor.mapping.v2.parser.antlr.MappingParser.targetType(MappingParser.java:524)
    at io.openapiprocessor.core.processor.mapping.v2.parser.antlr.MappingParser.map(MappingParser.java:276)
    at io.openapiprocessor.core.processor.mapping.v2.parser.antlr.MappingParser.mapping(MappingParser.java:154)
    at io.openapiprocessor.core.processor.mapping.v2.parser.antlr.ParserKt.parseMapping(Parser.kt:22)
    ... 47 more
Caused by: org.antlr.v4.runtime.NoViableAltException
    at org.antlr.v4.runtime.atn.ParserATNSimulator.noViableAlt(ParserATNSimulator.java:2031)
    at org.antlr.v4.runtime.atn.ParserATNSimulator.execATN(ParserATNSimulator.java:470)
    at org.antlr.v4.runtime.atn.ParserATNSimulator.adaptivePredict(ParserATNSimulator.java:396)
    at io.openapiprocessor.core.processor.mapping.v2.parser.antlr.MappingParser.targetType(MappingParser.java:510)
    ... 50 more
Caused by: io.openapiprocessor.core.processor.mapping.v2.parser.antlr.MappingParserException: no viable alternative at input 'java.util.Map<java.lang.String,java.lang.List<'

Caused by: org.antlr.v4.runtime.NoViableAltException

Execution failed for task '...:processSpring'.
> A failure occurred while executing io.openapiprocessor.gradle.OpenApiProcessorWorker
   > failed to parse mapping: FooToBar => java.util.Map<java.lang.String, java.lang.List<java.lang.String>>

It would be useful to add complex generic types support.

MetallFoX commented 1 year ago

According to swagger reference to openapi specification, the correct way to define Dictionary or Map is to specify additionalProperties section.

SpringDoc annotation processing generates something like this for the Map<String, List<String>> too:

  /foo/bar:
    get:
      tags:
        - foo
        - bar
      summary: foobar
      responses:
        '200':
          description: OK
          content:
            '*/*':
              schema:
                type: object
                additionalProperties:
                  type: array
                  items:
                    type: string

I've tried several definitions with no result and didn't find additionalProperties handling in the code either.

It might be better to add additionalProperties support instead of using custom mapping workarounds for such cases.

hauner commented 1 year ago

at the moment, only one level of generic is supported in the mapping (I think it is in the docs). I didn't spent any time yet to find out what it would take to support more levels.

hauner commented 1 year ago

yes, there is no code that checks additionalProperties. So let's make this a feature request :-)