Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] Invalid AsyncAPI documentation generated. #3901

Closed
MrMaxxan opened this issue Jul 4, 2024 · 3 comments
Closed

[BUG] Invalid AsyncAPI documentation generated. #3901

MrMaxxan opened this issue Jul 4, 2024 · 3 comments

Comments

@MrMaxxan
Copy link

MrMaxxan commented Jul 4, 2024

Tapir version: 1.10.12
Circe version: 0.14.3
Scala version: 2.13

When generating the documentation for the async API it generates documentation that is invalid if checked with a linter (for example https://studio.asyncapi.com/).
The error is error asyncapi-message-examples "payload" property type must be object components.messages.FunctionRegistryCommandMessage.examples[0].payload and the problematic part are the exampels.

examples:
     - payload:
       - header:
           createdAt: '2024-07-04T08:30:59.969609200Z'
           userId: f372893e-8bdc-4442-8923-d4a2017a29f7
           correlationId: 7b50f715-f079-42f7-b470-5fd5dc171639
           causationId: 3473393e-5798-4670-b766-cef254eabba5
           type: FunctionCreated
           tenantId: fa56e7b7-0cfe-413f-a51a-c67e4727231e
           itemId: 753d60ff-4dbd-496f-92be-5e072ceb2d74
           id: 01a74965-b487-4688-a5e6-4a8c97c56242
         data:
           FunctionCreated:
             fileId: description
             arguments:
             - id: c7f6ddbf-6a8d-4524-a577-16b962a520b5
               description: argument description
               type: Str
               config: null
             returns:
             - id: ce1e027c-907c-46de-8c51-61c2699803dd
               description: Return description
               type: Str
               config: null
             description: fileId
             name: name
             metadata:
               key: metadataValue
             commonId: 049b7cd5-9933-41a2-ad33-ec277ea14f1f
             id: f86eabcb-9f09-4027-b467-fdf80ef44abc
             owner: 80975d81-21b1-403d-a869-ba45c1cbe91a

Code used to generate the file.

class AsyncApiYamlGenerator extends CirceCodecs with SchemaDerivation {

  implicit val schemaForType: Schema[Type] =
    Schema(SString(), Some(SName("Type")), description = Some(typeDescription))

  private val commandEndpoints = endpoint
    .in(COMMANDS_TOPIC)
    .out(
      webSocketBody[FunctionRegistryCommandMessage, CodecFormat.Json, String, CodecFormat.TextPlain]
        (PekkoStreams)
        .requestsExamples(List(FunctionRegistryCommandMessage(commandHeaderExample, commandFunctionExample)))
    )

  private val eventEndpoints = endpoint
    .in(EVENTS_TOPIC)
    .out(
      webSocketBody[String, CodecFormat.TextPlain, FunctionRegistryEventMessage, CodecFormat.Json]
        (PekkoStreams)
        .responsesExamples(List(FunctionRegistryEventMessage(eventHeaderExample, eventFunctionExample)))
    )

  private val info: Info = Info("API", FuncregBuildInfo.version, Some("Some description"))

  // Since tapir AsyncAPI only support websockets, we have to remove some fields that are not applicable
  private val jsPathsToBeRemoved: Vector[JsPath] =
    Vector(
      JsPath \ "channels" \ "/exe.cmd.func.0" \ "subscribe",
      JsPath \ "components" \ "messages" \ "string"
    )

  // Async api only containing Funcreg api
  val asyncApiFuncregYaml: String = {
    val funcregAsyncApi: AsyncAPI =
      AsyncAPIInterpreter().toAsyncAPI(List(commandEndpoints, eventEndpoints), info)
    val asyncApiJsValue = Json.parse(funcregAsyncApi.asJson.toString())
    val asyncApiClean: JsValue =
      removeFields(jsPathsToBeRemoved, removeNullFields(asyncApiJsValue)).as[JsObject]
    io.circe.jawn.parse(Json.stringify(asyncApiClean)).valueOr(throw _).asYaml.spaces2
  }
}

And the result is

asyncapi: 2.6.0
info:
  title: API for Function Registry
  version: 0.115.0
  description: "Some description"
components:
  schemas:
    CloneEntity:
      required:
      - id
      - tenantId
      type: object
      properties:
        id:
          type: string
          format: uuid
        versionId:
          type: string
          format: uuid
        tenantId:
          type: string
          format: uuid
    FloatArgumentConfig:
      type: object
      properties:
        description:
          type: string
        optional:
          type: boolean
        min:
          type: number
          format: float
        max:
          type: number
          format: float
        defaultValue:
          type: number
          format: float
    EnumOption:
      required:
      - value
      type: object
      properties:
        value:
          type: string
        label:
          type: string
        description:
          type: string
    FileTypeConfig:
      type: object
      properties:
        description:
          type: string
        optional:
          type: boolean
    ArgumentConfig:
      oneOf:
      - $ref: '#/components/schemas/EnumArgumentConfig'
      - $ref: '#/components/schemas/FileTypeConfig'
      - $ref: '#/components/schemas/FloatArgumentConfig'
      - $ref: '#/components/schemas/IntArgumentConfig'
      - $ref: '#/components/schemas/JsonArgumentConfig'
      - $ref: '#/components/schemas/StrArgumentConfig'
    FunctionRegistryCommandMessage:
      required:
      - header
      - data
      type: object
      properties:
        header:
          $ref: '#/components/schemas/DocumentationCommandHeader'
        data:
          $ref: '#/components/schemas/Command'
    Map_String:
      type: object
      additionalProperties:
        type: string
    FunctionRegistryEventMessage:
      required:
      - header
      - data
      type: object
      properties:
        header:
          $ref: '#/components/schemas/EventHeader'
        data:
          $ref: '#/components/schemas/EventData'
    Argument:
      required:
      - description
      - type
      type: object
      properties:
        id:
          type: string
          format: uuid
        description:
          type: string
        type:
          $ref: '#/components/schemas/Type'
        config:
          $ref: '#/components/schemas/ArgumentConfig'
    IntArgumentConfig:
      type: object
      properties:
        defaultValue:
          type: integer
          format: int32
        description:
          type: string
        optional:
          type: boolean
        max:
          type: integer
          format: int32
        min:
          type: integer
          format: int32
    EnumArgumentConfig:
      type: object
      properties:
        description:
          type: string
        options:
          type: array
          items:
            $ref: '#/components/schemas/EnumOption'
        defaultOption:
          type: integer
          format: int32
        optional:
          type: boolean
    UpdateFunction:
      required:
      - id
      - commonId
      - parentId
      - fileId
      - name
      - description
      - metadata
      type: object
      properties:
        returns:
          type: array
          items:
            $ref: '#/components/schemas/Return'
        id:
          type: string
          format: uuid
        parentId:
          type: string
          format: uuid
        arguments:
          type: array
          items:
            $ref: '#/components/schemas/Argument'
        name:
          type: string
        commonId:
          type: string
          format: uuid
        description:
          type: string
        metadata:
          $ref: '#/components/schemas/Map_String'
        fileId:
          type: string
    EventData:
      oneOf:
      - $ref: '#/components/schemas/FunctionCreated'
      - $ref: '#/components/schemas/FunctionUpdated'
    StrArgumentConfig:
      type: object
      properties:
        description:
          type: string
        optional:
          type: boolean
    DocumentationCommandHeader:
      required:
      - id
      - type
      - createdAt
      - accessToken
      - itemId
      - correlationId
      - causationId
      type: object
      properties:
        itemId:
          type: string
          format: uuid
        accessToken:
          type: string
        causationId:
          type: string
          format: uuid
        id:
          type: string
          format: uuid
        type:
          type: string
        createdAt:
          type: string
          format: date-time
        correlationId:
          type: string
          format: uuid
    Type:
      type: string
      description: "Some description"
    EventHeader:
      required:
      - id
      - type
      - createdAt
      - userId
      - itemId
      - correlationId
      - causationId
      - tenantId
      type: object
      properties:
        createdAt:
          type: string
          format: date-time
        tenantId:
          type: string
          format: uuid
        type:
          type: string
        userId:
          type: string
          format: uuid
        itemId:
          type: string
          format: uuid
        id:
          type: string
          format: uuid
        causationId:
          type: string
          format: uuid
        correlationId:
          type: string
          format: uuid
    CloneDestinationEntity:
      required:
      - id
      type: object
      properties:
        id:
          type: string
          format: uuid
    JsonArgumentConfig:
      type: object
      properties:
        description:
          type: string
        defaultValue:
          type: string
        optional:
          type: boolean
    Return:
      required:
      - description
      - type
      type: object
      properties:
        id:
          type: string
          format: uuid
        description:
          type: string
        type:
          $ref: '#/components/schemas/Type'
        config:
          $ref: '#/components/schemas/ArgumentConfig'
    FunctionUpdated:
      required:
      - id
      - commonId
      - parentId
      - fileId
      - name
      - description
      - owner
      - metadata
      type: object
      properties:
        name:
          type: string
        description:
          type: string
        arguments:
          type: array
          items:
            $ref: '#/components/schemas/Argument'
        id:
          type: string
          format: uuid
        parentId:
          type: string
          format: uuid
        fileId:
          type: string
        owner:
          type: string
          format: uuid
        metadata:
          $ref: '#/components/schemas/Map_String'
        commonId:
          type: string
          format: uuid
        returns:
          type: array
          items:
            $ref: '#/components/schemas/Return'
    CreateFunction:
      required:
      - id
      - commonId
      - fileId
      - name
      - description
      - metadata
      type: object
      properties:
        arguments:
          type: array
          items:
            $ref: '#/components/schemas/Argument'
        name:
          type: string
        fileId:
          type: string
        metadata:
          $ref: '#/components/schemas/Map_String'
        description:
          type: string
        id:
          type: string
          format: uuid
        commonId:
          type: string
          format: uuid
        returns:
          type: array
          items:
            $ref: '#/components/schemas/Return'
    DeepCloneEntityCommand:
      required:
      - sourceEntity
      - targetEntity
      type: object
      properties:
        sourceEntity:
          $ref: '#/components/schemas/CloneEntity'
        targetEntity:
          $ref: '#/components/schemas/CloneEntity'
        destination:
          $ref: '#/components/schemas/CloneDestinationEntity'
    FunctionCreated:
      required:
      - id
      - commonId
      - fileId
      - name
      - description
      - owner
      - metadata
      type: object
      properties:
        owner:
          type: string
          format: uuid
        arguments:
          type: array
          items:
            $ref: '#/components/schemas/Argument'
        returns:
          type: array
          items:
            $ref: '#/components/schemas/Return'
        fileId:
          type: string
        metadata:
          $ref: '#/components/schemas/Map_String'
        id:
          type: string
          format: uuid
        commonId:
          type: string
          format: uuid
        description:
          type: string
        name:
          type: string
    Command:
      oneOf:
      - $ref: '#/components/schemas/CreateFunction'
      - $ref: '#/components/schemas/DeepCloneEntityCommand'
      - $ref: '#/components/schemas/UpdateFunction'
  messages:
    FunctionRegistryCommandMessage:
      payload:
        $ref: '#/components/schemas/FunctionRegistryCommandMessage'
      contentType: application/json
      examples:
      - payload:
        - header:
            itemId: 753d60ff-4dbd-496f-92be-5e072ceb2d74
            type: CreateFunction
            id: 3473393e-5798-4670-b766-cef254eabba5
            accessToken: token
            correlationId: 7b50f715-f079-42f7-b470-5fd5dc171639
            causationId: e97e35e8-69eb-431c-98c9-463ef262dae4
            createdAt: '2024-07-04T08:30:59.968609700Z'
          data:
            CreateFunction:
              fileId: description
              description: fileId
              commonId: 049b7cd5-9933-41a2-ad33-ec277ea14f1f
              returns:
              - id: ce1e027c-907c-46de-8c51-61c2699803dd
                description: Return description
                type: Str
                config: null
              name: name
              metadata:
                key: metadataValue
              id: 5d799dfa-afbe-4270-ad54-7dee34fe6ba6
              arguments:
              - id: c7f6ddbf-6a8d-4524-a577-16b962a520b5
                description: argument description
                type: Str
                config: null
    FunctionRegistryEventMessage:
      payload:
        $ref: '#/components/schemas/FunctionRegistryEventMessage'
      contentType: application/json
      examples:
      - payload:
        - header:
            createdAt: '2024-07-04T08:30:59.969609200Z'
            userId: f372893e-8bdc-4442-8923-d4a2017a29f7
            correlationId: 7b50f715-f079-42f7-b470-5fd5dc171639
            causationId: 3473393e-5798-4670-b766-cef254eabba5
            type: FunctionCreated
            tenantId: fa56e7b7-0cfe-413f-a51a-c67e4727231e
            itemId: 753d60ff-4dbd-496f-92be-5e072ceb2d74
            id: 01a74965-b487-4688-a5e6-4a8c97c56242
          data:
            FunctionCreated:
              fileId: description
              arguments:
              - id: c7f6ddbf-6a8d-4524-a577-16b962a520b5
                description: argument description
                type: Str
                config: null
              returns:
              - id: ce1e027c-907c-46de-8c51-61c2699803dd
                description: Return description
                type: Str
                config: null
              description: fileId
              name: name
              metadata:
                key: metadataValue
              commonId: 049b7cd5-9933-41a2-ad33-ec277ea14f1f
              id: f86eabcb-9f09-4027-b467-fdf80ef44abc
              owner: 80975d81-21b1-403d-a869-ba45c1cbe91a
channels:
  /execution.event.functions.0:
    subscribe:
      operationId: onExecution.event.functions.0
      message:
        $ref: '#/components/messages/FunctionRegistryEventMessage'
  /execution.cmd.functions.0:
    publish:
      operationId: sendExecution.cmd.functions.0
      message:
        $ref: '#/components/messages/FunctionRegistryCommandMessage'
@adamw
Copy link
Member

adamw commented Jul 15, 2024

I think that's fixed by softwaremill/sttp-apispec#174 ?

zorba128 added a commit to zorba128/tapir that referenced this issue Jul 16, 2024
zorba128 added a commit to zorba128/tapir that referenced this issue Jul 16, 2024
@MrMaxxan
Copy link
Author

I tried to update asyncapi-circe-yaml from 0.10.0 to 0.11.0 but when I do funcregAsyncApi.asJson I get this exception. I think I need to wait for Tapir to be updated as well since I read that it was a breaking change. 😊

Exception in thread "main" java.lang.ClassCastException: class scala.collection.immutable.Map$Map1 cannot be cast to class sttp.apispec.asyncapi.MessageExample (scala.collection.immutable.Map$Map1 and sttp.apispec.asyncapi.MessageExample are in unnamed module of loader 'app')
	at shapeless.Generic$$anon$1.to(generic.scala:163)
	at shapeless.LabelledGeneric$$anon$2.to(generic.scala:238)
	at shapeless.LabelledGeneric$$anon$2.to(generic.scala:236)
	at io.circe.generic.encoding.DerivedAsObjectEncoder$$anon$1.encodeObject(DerivedAsObjectEncoder.scala:29)
	at io.circe.Encoder$AsObject$$anon$69.encodeObject(Encoder.scala:924)
	at io.circe.Encoder$AsObject.apply(Encoder.scala:904)
	at io.circe.Encoder$AsObject.apply$(Encoder.scala:904)
	at io.circe.Encoder$AsObject$$anon$69.apply(Encoder.scala:923)
	at sttp.apispec.internal.JsonSchemaCirceEncoders.$anonfun$encodeList$2(JsonSchemaCirceEncoders.scala:182)
	at scala.collection.immutable.List.map(List.scala:246)
	at sttp.apispec.internal.JsonSchemaCirceEncoders.sttp$apispec$internal$JsonSchemaCirceEncoders$$$anonfun$encodeList$1(JsonSchemaCirceEncoders.scala:182)
	at sttp.apispec.internal.JsonSchemaCirceEncoders$$anonfun$encodeList$3.apply(JsonSchemaCirceEncoders.scala:180)
	at sttp.apispec.internal.JsonSchemaCirceEncoders$$anonfun$encodeList$3.apply(JsonSchemaCirceEncoders.scala:180)
	at sttp.apispec.asyncapi.circe.SttpAsyncAPICirceEncoders$anon$lazy$macro$63$1$$anon$22.encodeObject(package.scala:133)
	at sttp.apispec.asyncapi.circe.SttpAsyncAPICirceEncoders$anon$lazy$macro$63$1$$anon$22.encodeObject(package.scala:133)
	at io.circe.generic.encoding.DerivedAsObjectEncoder$$anon$1.encodeObject(DerivedAsObjectEncoder.scala:29)
	at io.circe.Encoder$AsObject$$anon$69.encodeObject(Encoder.scala:924)
	at io.circe.Encoder$AsObject.apply(Encoder.scala:904)
	at io.circe.Encoder$AsObject.apply$(Encoder.scala:904)
	at io.circe.Encoder$AsObject$$anon$69.apply(Encoder.scala:923)
	at sttp.apispec.asyncapi.circe.SttpAsyncAPICirceEncoders.sttp$apispec$asyncapi$circe$SttpAsyncAPICirceEncoders$$$anonfun$encoderMessage$1(package.scala:136)
	at sttp.apispec.asyncapi.circe.SttpAsyncAPICirceEncoders$$anonfun$encoderMessage$2.apply(package.scala:135)
	at sttp.apispec.asyncapi.circe.SttpAsyncAPICirceEncoders$$anonfun$encoderMessage$2.apply(package.scala:135)
	at sttp.apispec.asyncapi.circe.SttpAsyncAPICirceEncoders.sttp$apispec$asyncapi$circe$SttpAsyncAPICirceEncoders$$$anonfun$encoderReferenceOr$1(package.scala:29)
	at sttp.apispec.asyncapi.circe.SttpAsyncAPICirceEncoders$$anonfun$encoderReferenceOr$2.apply(package.scala:20)
	at sttp.apispec.asyncapi.circe.SttpAsyncAPICirceEncoders$$anonfun$encoderReferenceOr$2.apply(package.scala:20)
	at sttp.apispec.internal.JsonSchemaCirceEncoders.$anonfun$doEncodeListMap$2(JsonSchemaCirceEncoders.scala:190)
	at scala.collection.StrictOptimizedMapOps.map(StrictOptimizedMapOps.scala:28)
	at scala.collection.StrictOptimizedMapOps.map$(StrictOptimizedMapOps.scala:27)
	at scala.collection.immutable.ListMap.map(ListMap.scala:43)
	at sttp.apispec.internal.JsonSchemaCirceEncoders.sttp$apispec$internal$JsonSchemaCirceEncoders$$$anonfun$doEncodeListMap$1(JsonSchemaCirceEncoders.scala:190)
	at sttp.apispec.internal.JsonSchemaCirceEncoders$$anonfun$doEncodeListMap$3.apply(JsonSchemaCirceEncoders.scala:187)
	at sttp.apispec.internal.JsonSchemaCirceEncoders$$anonfun$doEncodeListMap$3.apply(JsonSchemaCirceEncoders.scala:187)
	at sttp.apispec.asyncapi.circe.SttpAsyncAPICirceEncoders$anon$lazy$macro$35$1$$anon$27.encodeObject(package.scala:144)
	at sttp.apispec.asyncapi.circe.SttpAsyncAPICirceEncoders$anon$lazy$macro$35$1$$anon$27.encodeObject(package.scala:144)
	at io.circe.generic.encoding.DerivedAsObjectEncoder$$anon$1.encodeObject(DerivedAsObjectEncoder.scala:29)
	at io.circe.Encoder$AsObject$$anon$69.encodeObject(Encoder.scala:924)
	at io.circe.Encoder$AsObject.apply(Encoder.scala:904)
	at io.circe.Encoder$AsObject.apply$(Encoder.scala:904)
	at io.circe.Encoder$AsObject$$anon$69.apply(Encoder.scala:923)
	at io.circe.Encoder$$anon$22.apply(Encoder.scala:362)
	at io.circe.Encoder$$anon$22.apply(Encoder.scala:360)
	at sttp.apispec.asyncapi.circe.SttpAsyncAPICirceEncoders$anon$lazy$macro$39$4$$anon$33.encodeObject(package.scala:151)
	at sttp.apispec.asyncapi.circe.SttpAsyncAPICirceEncoders$anon$lazy$macro$39$4$$anon$33.encodeObject(package.scala:151)
	at io.circe.generic.encoding.DerivedAsObjectEncoder$$anon$1.encodeObject(DerivedAsObjectEncoder.scala:29)
	at io.circe.Encoder$AsObject$$anon$69.encodeObject(Encoder.scala:924)
	at io.circe.Encoder$AsObject.apply(Encoder.scala:904)
	at io.circe.Encoder$AsObject.apply$(Encoder.scala:904)
	at io.circe.Encoder$AsObject$$anon$69.apply(Encoder.scala:923)
	at io.circe.syntax.package$EncoderOps$.asJson$extension(package.scala:24)
	at com.sartorius.funcreg.utils.AsyncApiYamlGenerator.<init>(AsyncApiYamlGenerator.scala:81)
	at com.sartorius.funcreg.utils.ApiYamlGenerator$.delayedEndpoint$com$sartorius$funcreg$utils$ApiYamlGenerator$1(ApiYamlGenerator.scala:12)
	at com.sartorius.funcreg.utils.ApiYamlGenerator$delayedInit$body.apply(ApiYamlGenerator.scala:6)
	at scala.Function0.apply$mcV$sp(Function0.scala:42)
	at scala.Function0.apply$mcV$sp$(Function0.scala:42)
	at scala.runtime.AbstractFunction0.apply$mcV$sp(AbstractFunction0.scala:17)
	at scala.App.$anonfun$main$1(App.scala:98)
	at scala.App.$anonfun$main$1$adapted(App.scala:98)
	at scala.collection.IterableOnceOps.foreach(IterableOnce.scala:576)
	at scala.collection.IterableOnceOps.foreach$(IterableOnce.scala:574)
	at scala.collection.AbstractIterable.foreach(Iterable.scala:933)
	at scala.App.main(App.scala:98)
	at scala.App.main$(App.scala:96)
	at com.sartorius.funcreg.utils.ApiYamlGenerator$.main(ApiYamlGenerator.scala:6)
	at com.sartorius.funcreg.utils.ApiYamlGenerator.main(ApiYamlGenerator.scala)

@adamw
Copy link
Member

adamw commented Jul 19, 2024

Released in latest tapir, I'll close this, please reopen if the issue persists.

@adamw adamw closed this as completed Jul 19, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants