diff --git a/cli/example/simple-ref.yaml b/cli/example/simple-ref.yaml new file mode 100644 index 00000000..d57e6653 --- /dev/null +++ b/cli/example/simple-ref.yaml @@ -0,0 +1,29 @@ +openapi: 3.0.1 +info: + title: "Simple Ref" + version: 1.0.0 +components: + schemas: + Forbidden: + $ref: "#/components/schemas/Message" + Message: + type: object + properties: + msg: + type: string + required: + - msg + + responses: + Forbidden: + description: Wrong credentials + content: + application/json: + schema: + $ref: "#/components/schemas/Forbidden" +paths: + "/api": + get: + responses: + 200: + $ref: "#/components/responses/Forbidden" diff --git a/cli/example/src/Example.elm b/cli/example/src/Example.elm index e2ee0ea6..b0218761 100644 --- a/cli/example/src/Example.elm +++ b/cli/example/src/Example.elm @@ -2,7 +2,7 @@ module Example exposing (..) import AdditionalProperties.Types import AirlineCodeLookupApi.Api -import AirlineCodeLookupApi.Types +import AirlineCodeLookupApi.Types.Responses import Browser import DbFahrplanApi.Api import DbFahrplanApi.Types @@ -14,11 +14,13 @@ import MarioPartyStats.Types import NullableEnum.Json import OpenApi.Common import PatreonApi.Api -import PatreonApi.Types +import PatreonApi.Types.Responses import RealworldConduitApi.Api -import RealworldConduitApi.Types +import RealworldConduitApi.Types.Responses import RecursiveAllofRefs.Types +import SimpleRef.Json import SingleEnum.Types +import Trustmark.Api import Trustmark.TradeCheck.Api import Trustmark.TradeCheck.Servers import Trustmark.TradeCheck.Types @@ -40,6 +42,10 @@ type alias Model = init : () -> ( Model, Cmd Msg ) init () = + let + _ = + SimpleRef.Json.decodeForbidden + in ( {} , Cmd.batch [ RealworldConduitApi.Api.getArticle @@ -85,6 +91,13 @@ init () = { server = Trustmark.TradeCheck.Servers.sandbox , authorization = { x_api_key = "?" } , body = { publicId = 0, schemeId = Nothing } + , toMsg = TrustmarkTradeCheckResponse + } + , Trustmark.Api.taxonomiesDocumentTypes + { authorization = + { tm_api_key = "?" + , x_api_key = "?" + } , toMsg = TrustmarkResponse } , PatreonApi.Api.getCampaign @@ -109,14 +122,15 @@ subscriptions _ = type Msg - = ConduitResponse (Result (OpenApi.Common.Error RealworldConduitApi.Types.GenericError String) RealworldConduitApi.Types.SingleArticleResponse) - | AmadeusResponse (Result (OpenApi.Common.Error AirlineCodeLookupApi.Types.Getairlines_Error String) AirlineCodeLookupApi.Types.Airlines) + = ConduitResponse (Result (OpenApi.Common.Error RealworldConduitApi.Types.Responses.GenericError String) RealworldConduitApi.Types.Responses.SingleArticleResponse) + | AmadeusResponse (Result (OpenApi.Common.Error AirlineCodeLookupApi.Types.Responses.Getairlines_Error String) AirlineCodeLookupApi.Types.Responses.Airlines) -- | BimResponse (Result (OpenApi.Common.Error BimcloudApi20232AlphaRelease.BlobStoreService10BeginBatchUpload_Error Bytes.Bytes) Bytes.Bytes) | GithubResponse (Result (OpenApi.Common.Error () String) GithubV3RestApi.Types.Root) | DbFahrplanResponse (Result (OpenApi.Common.Error Never String) DbFahrplanApi.Types.LocationResponse) | MarioPartyStatsResponse (Result (OpenApi.Common.Error Never String) (List MarioPartyStats.Types.Boards)) - | TrustmarkResponse (Result (OpenApi.Common.Error Never String) Trustmark.TradeCheck.Types.TradeCheckResponse) - | PatreonResponse (Result (OpenApi.Common.Error PatreonApi.Types.GetCampaign_Error String) PatreonApi.Types.CampaignResponse) + | TrustmarkTradeCheckResponse (Result (OpenApi.Common.Error Never String) Trustmark.TradeCheck.Types.TradeCheckResponse) + | PatreonResponse (Result (OpenApi.Common.Error PatreonApi.Types.Responses.GetCampaign_Error String) PatreonApi.Types.Responses.CampaignResponse) + | TrustmarkResponse (Result (OpenApi.Common.Error Never String) (List String)) update : Msg -> Model -> ( Model, Cmd Msg ) @@ -143,6 +157,9 @@ update msg model = PatreonResponse _ -> ( model, Cmd.none ) + TrustmarkTradeCheckResponse _ -> + ( model, Cmd.none ) + view : Model -> Browser.Document Msg view _ = diff --git a/cli/src/TestGenScript.elm b/cli/src/TestGenScript.elm index 7538ec95..4eca14b6 100644 --- a/cli/src/TestGenScript.elm +++ b/cli/src/TestGenScript.elm @@ -106,6 +106,10 @@ run = telegramBot = OpenApi.Config.inputFrom (OpenApi.Config.File "./example/telegram-bot.json") + simpleRef : OpenApi.Config.Input + simpleRef = + OpenApi.Config.inputFrom (OpenApi.Config.File "./example/simple-ref.yaml") + bug : Int -> OpenApi.Config.Input bug n = OpenApi.Config.inputFrom (OpenApi.Config.File ("./example/openapi-generator-bugs/" ++ String.fromInt n ++ ".yaml")) @@ -130,6 +134,7 @@ run = |> OpenApi.Config.withInput binaryResponse |> OpenApi.Config.withInput nullableEnum |> OpenApi.Config.withInput cookieAuth + |> OpenApi.Config.withInput simpleRef |> OpenApi.Config.withInput (bug 7889) |> OpenApi.Config.withInput (bug 10398) |> OpenApi.Config.withInput (bug 16104) diff --git a/elm.json b/elm.json index c9b96c8b..9ff10257 100644 --- a/elm.json +++ b/elm.json @@ -23,7 +23,11 @@ "wolfadex/elm-open-api": "2.0.0 <= v < 3.0.0" }, "test-dependencies": { - "elm-explorations/test": "2.2.0 <= v < 3.0.0", - "miniBill/elm-unicode": "1.1.1 <= v < 2.0.0" + "arowM/elm-multiline-string": "1.0.0 <= v < 2.0.0", + "elm-explorations/test": "2.2.1 <= v < 3.0.0", + "miniBill/elm-diff": "1.1.1 <= v < 2.0.0", + "miniBill/elm-unicode": "1.1.1 <= v < 2.0.0", + "wolfadex/elm-ansi": "3.0.1 <= v < 4.0.0", + "MaybeJustJames/yaml": "2.1.7 <= v < 3.0.0" } } diff --git a/package-lock.json b/package-lock.json index 1311d3bd..5c59cf17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "elm-optimize-level-2": "0.3.5", "elm-pages": "^3.0.28", "elm-review": "^2.13.5", - "elm-test": "^0.19.1-revision16", + "elm-test": "^0.19.1-revision17", "vite": "^6.0.3" } }, @@ -3737,9 +3737,9 @@ "license": "MPL-2.0" }, "node_modules/elm-test": { - "version": "0.19.1-revision16", - "resolved": "https://registry.npmjs.org/elm-test/-/elm-test-0.19.1-revision16.tgz", - "integrity": "sha512-t4SCY3Vq2KUuUX9LpqmITroutY/l5hjLeQ8Hpc1bUdzqzTifxVs76omjfdHHZJGUCLPVUN0VktCNsNr36IUcaw==", + "version": "0.19.1-revision17", + "resolved": "https://registry.npmjs.org/elm-test/-/elm-test-0.19.1-revision17.tgz", + "integrity": "sha512-P9J8tXdLXmqgIMlSozfcb/KCORq4GuxfwBbzqFLMss8KlGmUKonOJwseRX6xVkdU5xgGVA/mz9rfMTNomIbwng==", "dev": true, "license": "BSD-3-Clause", "dependencies": { diff --git a/package.json b/package.json index 4b76fb8b..a9515f25 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "elm-optimize-level-2": "0.3.5", "elm-pages": "^3.0.28", "elm-review": "^2.13.5", - "elm-test": "^0.19.1-revision16", + "elm-test": "^0.19.1-revision17", "vite": "^6.0.3" }, "volta": { diff --git a/review/suppressed/NoMissingTypeExpose.json b/review/suppressed/NoMissingTypeExpose.json index 56dc4277..51081ad3 100644 --- a/review/suppressed/NoMissingTypeExpose.json +++ b/review/suppressed/NoMissingTypeExpose.json @@ -3,7 +3,6 @@ "automatically created by": "elm-review suppress", "learn more": "elm-review suppress --help", "suppressions": [ - { "count": 4, "filePath": "src/OpenApi/Config.elm" }, - { "count": 1, "filePath": "src/OpenApi/Generate.elm" } + { "count": 4, "filePath": "src/OpenApi/Config.elm" } ] } diff --git a/src/CliMonad.elm b/src/CliMonad.elm index 5f980042..0ffd7205 100644 --- a/src/CliMonad.elm +++ b/src/CliMonad.elm @@ -1,20 +1,21 @@ module CliMonad exposing ( CliMonad, Message, OneOfName, Path, Declaration , run, stepOrFail - , succeed, succeedWith, fail + , succeed, succeedWith, fail, fromResult , map, map2, map3 , andThen, andThen2, andThen3, andThen4, combine, combineDict, combineMap, foldl , errorToWarning, getApiSpec, enumName, moduleToNamespace, getOrCache , withPath, withWarning, withExtendedWarning, withRequiredPackage , todo, todoWithDefault , withFormat + , nameToAnnotation, refToAnnotation, refToEncoder, refToDecoder ) {-| @docs CliMonad, Message, OneOfName, Path, Declaration @docs run, stepOrFail -@docs succeed, succeedWith, fail +@docs succeed, succeedWith, fail, fromResult @docs map, map2, map3 @docs andThen, andThen2, andThen3, andThen4, combine, combineDict, combineMap, foldl @docs errorToWarning, getApiSpec, enumName, moduleToNamespace, getOrCache @@ -22,6 +23,11 @@ module CliMonad exposing @docs todo, todoWithDefault @docs withFormat + +## Utils + +@docs nameToAnnotation, refToAnnotation, refToEncoder, refToDecoder + -} import Common @@ -83,11 +89,11 @@ type alias Input = type CliMonad a - = CliMonad - (Input - -> FastDict.Dict (List String) Common.Type - -> Result Message ( a, Output, FastDict.Dict (List String) Common.Type ) - ) + = CliMonad (Input -> Cache -> Result Message ( a, Output, Cache )) + + +type alias Cache = + FastDict.Dict String Common.Type type alias Output = @@ -144,9 +150,9 @@ run oneOfDeclarations input (CliMonad x) = , warnOnMissingEnums = input.warnOnMissingEnums } - res : Result Message ( List Declaration, Output, FastDict.Dict (List String) Common.Type ) + res : Result Message ( List Declaration, Output, Cache ) res = - x internalInput FastDict.empty + x internalInput emptyCache in res |> Result.andThen @@ -182,8 +188,17 @@ run oneOfDeclarations input (CliMonad x) = ) -getOrCache : List String -> (() -> CliMonad Common.Type) -> CliMonad Common.Type -getOrCache key compute = +emptyCache : Cache +emptyCache = + FastDict.empty + + +getOrCache : Common.RefTo Common.Schema -> (() -> CliMonad Common.Type) -> CliMonad Common.Type +getOrCache ref compute = + let + (Common.UnsafeName key) = + Common.refToString ref + in CliMonad (\input cache -> case FastDict.get key cache of @@ -499,8 +514,8 @@ combineMap f ls = List a -> List b -> Output - -> FastDict.Dict (List String) Common.Type - -> Result Message ( List b, Output, FastDict.Dict (List String) Common.Type ) + -> Cache + -> Result Message ( List b, Output, Cache ) go queue acc output accCache = case queue of [] -> @@ -531,8 +546,8 @@ combine ls = List (CliMonad a) -> List a -> Output - -> FastDict.Dict (List String) Common.Type - -> Result Message ( List a, Output, FastDict.Dict (List String) Common.Type ) + -> Cache + -> Result Message ( List a, Output, Cache ) go queue acc output accCache = case queue of [] -> @@ -862,3 +877,74 @@ withRequiredPackage package (CliMonad f) = ) (f input cache) ) + + +fromResult : Result String a -> CliMonad a +fromResult res = + case res of + Ok o -> + succeed o + + Err e -> + fail e + + +refToDecoder : Common.RefTo component -> CliMonad Elm.Expression +refToDecoder ref = + let + ( component, name ) = + Common.unwrapRef ref + in + map2 + (\importFrom ann -> + Elm.value + { importFrom = importFrom + , name = "decode" ++ Common.toTypeName name + , annotation = Just (Gen.Json.Decode.annotation_.decoder ann) + } + ) + (moduleToNamespace (Common.Json component)) + (refToAnnotation ref) + + +refToEncoder : Common.RefTo component -> CliMonad (Elm.Expression -> Elm.Expression) +refToEncoder ref = + let + ( component, name ) = + Common.unwrapRef ref + in + map2 + (\importFrom ann rec -> + Elm.apply + (Elm.value + { importFrom = importFrom + , name = "encode" ++ Common.toTypeName name + , annotation = Just (Elm.Annotation.function [ ann ] Gen.Json.Encode.annotation_.value) + } + ) + [ rec ] + ) + (moduleToNamespace (Common.Json component)) + (refToAnnotation ref) + |> withPath (Common.refToString ref) + + +refToAnnotation : Common.RefTo schema -> CliMonad Elm.Annotation.Annotation +refToAnnotation ref = + let + ( component, name ) = + Common.unwrapRef ref + in + nameToAnnotation component name + |> withPath (Common.refToString ref) + + +nameToAnnotation : Common.Component -> Common.UnsafeName -> CliMonad Elm.Annotation.Annotation +nameToAnnotation component name = + moduleToNamespace (Common.Types component) + |> map + (\importFrom -> + Elm.Annotation.named + importFrom + (Common.toTypeName name) + ) diff --git a/src/Common.elm b/src/Common.elm index 9cd40ff9..40ccd313 100644 --- a/src/Common.elm +++ b/src/Common.elm @@ -1,11 +1,16 @@ module Common exposing ( BasicType(..) + , Component(..) , ConstValue(..) , Field , Module(..) , Object , OneOfData , Package(..) + , RefTo + , RequestBody + , Response + , Schema , Type(..) , TypeName , UnsafeName(..) @@ -15,13 +20,20 @@ module Common exposing , commonModuleName , enum , moduleToNamespace - , ref + , parseRef + , parseRequestBodyRef + , parseResponseRef + , parseSchemaRef + , refTo + , refToString , reservedList , toTypeName , toValueName + , unwrapRef , unwrapUnsafe ) +import Json.Encode import NonEmpty import Regex import Set exposing (Set) @@ -29,14 +41,26 @@ import String.Extra type Module - = Json - | Types + = Json Component + | Types Component -- Nothing if we are only generating effects for a single package | Api (Maybe Package) | Common | Servers +type Component + = Schema + | Parameter + | SecurityScheme + | RequestBody + | Response + | Header + | Example + | Link + | Callback + + type Package = ElmHttp | DillonkearnsElmPages @@ -52,12 +76,18 @@ type UnsafeName moduleToNamespace : List String -> Module -> List String moduleToNamespace namespace moduleName = case moduleName of - Json -> + Json Schema -> namespace ++ [ "Json" ] - Types -> + Json component -> + namespace ++ [ "Json", componentToModulePiece component ] + + Types Schema -> namespace ++ [ "Types" ] + Types component -> + namespace ++ [ "Types", componentToModulePiece component ] + Api package -> case package of Just ElmHttp -> @@ -79,6 +109,102 @@ moduleToNamespace namespace moduleName = commonModuleName +componentToModulePiece : Component -> String +componentToModulePiece component = + case component of + Schema -> + "Schemas" + + Parameter -> + "Parameters" + + SecurityScheme -> + "SecuritySchemes" + + RequestBody -> + "RequestBodies" + + Response -> + "Responses" + + Header -> + "Headers" + + Example -> + "Examples" + + Link -> + "Links" + + Callback -> + "Callbacks" + + +componentFromString : String -> Maybe Component +componentFromString component = + case component of + "schemas" -> + Just Schema + + "parameters" -> + Just Parameter + + "securitySchemes" -> + Just SecurityScheme + + "requestBodies" -> + Just RequestBody + + "responses" -> + Just Response + + "headers" -> + Just Header + + "examples" -> + Just Example + + "links" -> + Just Link + + "callbacks" -> + Just Callback + + _ -> + Nothing + + +componentToString : Component -> String +componentToString component = + case component of + Schema -> + "schemas" + + Parameter -> + "parameters" + + SecurityScheme -> + "securitySchemes" + + RequestBody -> + "requestBodies" + + Response -> + "responses" + + Header -> + "headers" + + Example -> + "examples" + + Link -> + "links" + + Callback -> + "callbacks" + + commonModuleName : List String commonModuleName = [ "OpenApi", "Common" ] @@ -336,11 +462,27 @@ type Type | OneOf TypeName ( OneOfData, List OneOfData ) | Enum ( UnsafeName, List UnsafeName ) | Value - | Ref (List String) + | Ref (RefTo Schema) | Bytes | Unit +type RefTo r + = RefTo Component UnsafeName + + +type Schema + = TypeLevelSchema Never + + +type RequestBody + = TypeLevelRequestBody Never + + +type Response + = TypeLevelResponse Never + + type BasicType = Integer | Boolean @@ -397,9 +539,58 @@ type alias Field = } -ref : String -> Type -ref str = - Ref (String.split "/" str) +parseRef : String -> Result String (RefTo ()) +parseRef ref = + case String.split "/" ref of + [ "#", "components", component, name ] -> + case componentFromString component of + Just c -> + Ok (RefTo c (UnsafeName name)) + + Nothing -> + Err ("Invalid component: " ++ component) + + _ -> + Err ("Couldn't parse the ref " ++ Json.Encode.encode 0 (Json.Encode.string ref)) + + +parseSchemaRef : String -> Result String (RefTo Schema) +parseSchemaRef ref = + parseRef ref + |> Result.andThen + (\(RefTo component res) -> + if component == Schema then + Ok (RefTo Schema res) + + else + Err ("Expected a reference to a schema, found a reference to " ++ ref) + ) + + +parseRequestBodyRef : String -> Result String (RefTo RequestBody) +parseRequestBodyRef ref = + parseRef ref + |> Result.andThen + (\(RefTo component res) -> + if component == RequestBody then + Ok (RefTo RequestBody res) + + else + Err ("Expected a reference to a schema, found a reference to " ++ ref) + ) + + +parseResponseRef : String -> Result String (RefTo Response) +parseResponseRef ref = + parseRef ref + |> Result.andThen + (\(RefTo component res) -> + if component == Response then + Ok (RefTo Response res) + + else + Err ("Expected a reference to a schema, found a reference to " ++ ref) + ) unwrapUnsafe : UnsafeName -> String @@ -418,3 +609,18 @@ enum variants = base64PackageName : String base64PackageName = "danfishgold/base64-bytes" + + +refToString : RefTo component -> UnsafeName +refToString (RefTo component (UnsafeName name)) = + UnsafeName (String.join "/" [ "#", "components", componentToString component, name ]) + + +unwrapRef : RefTo component -> ( Component, UnsafeName ) +unwrapRef (RefTo component ref) = + ( component, ref ) + + +refTo : Component -> UnsafeName -> RefTo () +refTo component name = + RefTo component name diff --git a/src/JsonSchema/Generate.elm b/src/JsonSchema/Generate.elm index 3737335f..e8ce9db0 100644 --- a/src/JsonSchema/Generate.elm +++ b/src/JsonSchema/Generate.elm @@ -16,8 +16,8 @@ import NonEmpty import SchemaUtils -schemaToDeclarations : Common.UnsafeName -> Json.Schema.Definitions.Schema -> CliMonad (List CliMonad.Declaration) -schemaToDeclarations name schema = +schemaToDeclarations : Common.Component -> Common.UnsafeName -> Json.Schema.Definitions.Schema -> CliMonad (List CliMonad.Declaration) +schemaToDeclarations component name schema = SchemaUtils.schemaToType [] schema |> CliMonad.andThen (\{ type_, documentation } -> @@ -28,7 +28,7 @@ schemaToDeclarations name schema = in case type_ of Common.Enum enumVariants -> - [ { moduleName = Common.Types + [ { moduleName = Common.Types component , name = typeName , declaration = enumVariants @@ -46,7 +46,7 @@ schemaToDeclarations name schema = , group = "Enum" } |> CliMonad.succeed - , { moduleName = Common.Types + , { moduleName = Common.Types component , name = Common.toValueName name ++ "ToString" , declaration = Elm.fn (Elm.Arg.var "value") @@ -69,7 +69,7 @@ schemaToDeclarations name schema = , group = "Enum" } |> CliMonad.succeed - , { moduleName = Common.Types + , { moduleName = Common.Types component , name = Common.toValueName name ++ "FromString" , declaration = Elm.fn (Elm.Arg.varWith "value" Elm.Annotation.string) @@ -99,7 +99,7 @@ schemaToDeclarations name schema = , group = "Enum" } |> CliMonad.succeed - , { moduleName = Common.Types + , { moduleName = Common.Types component , name = Common.toValueName name ++ "Variants" , declaration = enumVariants @@ -120,7 +120,7 @@ schemaToDeclarations name schema = |> CliMonad.succeed , CliMonad.map (\importFrom -> - { moduleName = Common.Json + { moduleName = Common.Json component , name = "decode" ++ typeName , declaration = Elm.declaration @@ -153,10 +153,10 @@ schemaToDeclarations name schema = , group = "Decoders" } ) - (CliMonad.moduleToNamespace Common.Types) + (CliMonad.moduleToNamespace (Common.Types component)) , CliMonad.map (\importFrom -> - { moduleName = Common.Json + { moduleName = Common.Json component , name = "encode" ++ typeName , declaration = Elm.declaration ("encode" ++ typeName) @@ -178,7 +178,7 @@ schemaToDeclarations name schema = , group = "Encoders" } ) - (CliMonad.moduleToNamespace Common.Types) + (CliMonad.moduleToNamespace (Common.Types component)) ] |> CliMonad.combine @@ -191,7 +191,7 @@ schemaToDeclarations name schema = CliMonad.succeed [] else - [ { moduleName = Common.Types + [ { moduleName = Common.Types component , name = typeName , declaration = Elm.alias typeName annotation @@ -208,7 +208,7 @@ schemaToDeclarations name schema = |> CliMonad.succeed , CliMonad.map2 (\importFrom schemaDecoder -> - { moduleName = Common.Json + { moduleName = Common.Json component , name = "decode" ++ typeName , declaration = Elm.declaration @@ -220,11 +220,11 @@ schemaToDeclarations name schema = , group = "Decoders" } ) - (CliMonad.moduleToNamespace Common.Types) + (CliMonad.moduleToNamespace (Common.Types component)) (schemaToDecoder schema) , CliMonad.map2 (\importFrom encoder -> - { moduleName = Common.Json + { moduleName = Common.Json component , name = "encode" ++ typeName , declaration = Elm.declaration ("encode" ++ typeName) @@ -235,7 +235,7 @@ schemaToDeclarations name schema = , group = "Encoders" } ) - (CliMonad.moduleToNamespace Common.Types) + (CliMonad.moduleToNamespace (Common.Types component)) (schemaToEncoder schema) ] |> CliMonad.combine diff --git a/src/OpenApi/Generate.elm b/src/OpenApi/Generate.elm index b8c99ace..6764ac52 100644 --- a/src/OpenApi/Generate.elm +++ b/src/OpenApi/Generate.elm @@ -1,4 +1,4 @@ -module OpenApi.Generate exposing (ContentSchema(..), Message, Path, Mime, files) +module OpenApi.Generate exposing (ContentSchema, Message, Path, Mime, files) {-| @@ -86,6 +86,7 @@ type ContentSchema | StringContent Mime | BytesContent Mime | Base64Content Mime + | ReferenceContent (Common.RefTo Common.RequestBody) type alias AuthorizationInfo = @@ -131,7 +132,9 @@ files { namespace, generateTodos, effectTypes, server, formats, warnOnMissingEnu , schemasDeclarations , responsesDeclarations , requestBodiesDeclarations - , CliMonad.succeed (serverDeclarations info) + , serverDeclarations info + |> CliMonad.succeed + |> CliMonad.withPath (Common.UnsafeName "servers") ] |> CliMonad.combine ) @@ -296,6 +299,7 @@ pathDeclarations effectTypes server = |> CliMonad.map (List.filterMap identity >> List.concat) ) |> CliMonad.map List.concat + |> CliMonad.withPath (Common.UnsafeName "paths") ) @@ -315,6 +319,7 @@ responsesDeclarations = ) (CliMonad.succeed []) |> CliMonad.map List.concat + |> CliMonad.withPath (Common.UnsafeName "responses") ) @@ -334,6 +339,7 @@ requestBodiesDeclarations = ) (CliMonad.succeed []) |> CliMonad.map List.concat + |> CliMonad.withPath (Common.UnsafeName "requestBodies") ) @@ -350,21 +356,25 @@ schemasDeclarations = (\name schema -> CliMonad.map2 (\decls declAcc -> decls ++ declAcc) - (JsonSchema.Generate.schemaToDeclarations (Common.UnsafeName name) (OpenApi.Schema.get schema)) + (JsonSchema.Generate.schemaToDeclarations Common.Schema + (Common.UnsafeName name) + (OpenApi.Schema.get schema) + ) ) (CliMonad.succeed []) + |> CliMonad.withPath (Common.UnsafeName "schemas") ) -unitDeclarations : Common.UnsafeName -> CliMonad (List CliMonad.Declaration) -unitDeclarations name = +unitDeclarations : Common.Component -> Common.UnsafeName -> CliMonad (List CliMonad.Declaration) +unitDeclarations component name = let typeName : Common.TypeName typeName = Common.toTypeName name in CliMonad.combine - [ { moduleName = Common.Types + [ { moduleName = Common.Types component , name = typeName , declaration = Elm.alias typeName Elm.Annotation.unit @@ -374,7 +384,7 @@ unitDeclarations name = |> CliMonad.succeed , CliMonad.map2 (\importFrom schemaDecoder -> - { moduleName = Common.Json + { moduleName = Common.Json component , name = "decode" ++ typeName , declaration = Elm.declaration ("decode" ++ typeName) @@ -385,11 +395,11 @@ unitDeclarations name = , group = "Decoders" } ) - (CliMonad.moduleToNamespace Common.Types) + (CliMonad.moduleToNamespace (Common.Types component)) (SchemaUtils.typeToDecoder Common.Unit) , CliMonad.map2 (\importFrom encoder -> - { moduleName = Common.Json + { moduleName = Common.Json component , name = "encode" ++ typeName , declaration = Elm.declaration ("encode" ++ typeName) @@ -400,7 +410,7 @@ unitDeclarations name = , group = "Encoders" } ) - (CliMonad.moduleToNamespace Common.Types) + (CliMonad.moduleToNamespace (Common.Types component)) (SchemaUtils.typeToEncoder Common.Unit) ] @@ -416,12 +426,12 @@ responseToDeclarations name reference = in if Dict.isEmpty content then -- If there is no input content then we go with the unit value, `()` as the response type - unitDeclarations name + unitDeclarations Common.Response name else responseToSchema response |> CliMonad.withPath name - |> CliMonad.andThen (JsonSchema.Generate.schemaToDeclarations name) + |> CliMonad.andThen (JsonSchema.Generate.schemaToDeclarations Common.Response name) Nothing -> CliMonad.fail "Could not convert reference to concrete value" @@ -438,13 +448,13 @@ requestBodyToDeclarations name reference = OpenApi.RequestBody.content requestBody in if Dict.isEmpty content then - -- If there is no content then we go with the unit value, `()` as the requestBody type - unitDeclarations name + -- If there is no content then we go with the unit value, `()` as the requestBody type + unitDeclarations Common.RequestBody name else requestBodyToSchema requestBody |> CliMonad.withPath name - |> CliMonad.andThen (JsonSchema.Generate.schemaToDeclarations name) + |> CliMonad.andThen (JsonSchema.Generate.schemaToDeclarations Common.RequestBody name) Nothing -> CliMonad.fail "Could not convert reference to concrete value" @@ -554,27 +564,54 @@ toRequestFunctions server effectTypes method pathUrl operation = , lamderaProgramTest = toBody Gen.Effect.Http.call_.stringBody } + ReferenceContent _ -> + CliMonad.map + (\e _ -> + { core = e + , elmPages = e + , lamderaProgramTest = e + } + ) + (CliMonad.todo "toRequestFunctions: branch 'ReferenceContent _' not implemented") + bodyParams : ContentSchema -> CliMonad (List ( Common.UnsafeName, Elm.Annotation.Annotation )) bodyParams contentSchema = - case contentSchema of - EmptyContent -> - CliMonad.succeed [] - - JsonContent type_ -> - SchemaUtils.typeToAnnotationWithNullable type_ - |> CliMonad.map (\annotation -> [ ( Common.UnsafeName "body", annotation ) ]) - - StringContent _ -> - CliMonad.succeed [ ( Common.UnsafeName "body", Elm.Annotation.string ) ] - - BytesContent _ -> - CliMonad.succeed [ ( Common.UnsafeName "body", Gen.Bytes.annotation_.bytes ) ] - |> CliMonad.withRequiredPackage "elm/bytes" + let + annotation : CliMonad (Maybe Elm.Annotation.Annotation) + annotation = + case contentSchema of + EmptyContent -> + CliMonad.succeed Nothing + + JsonContent type_ -> + SchemaUtils.typeToAnnotationWithNullable type_ + |> CliMonad.map Just + + StringContent _ -> + CliMonad.succeed (Just Elm.Annotation.string) + + BytesContent _ -> + CliMonad.succeed (Just Gen.Bytes.annotation_.bytes) + |> CliMonad.withRequiredPackage "elm/bytes" + + Base64Content _ -> + CliMonad.succeed (Just Gen.Bytes.annotation_.bytes) + |> CliMonad.withRequiredPackage "elm/bytes" + |> CliMonad.withRequiredPackage Common.base64PackageName + + ReferenceContent _ -> + CliMonad.fail "toRequestFunctions: branch 'ReferenceContent _' not implemented" + in + annotation + |> CliMonad.map + (\maybeAnnotation -> + case maybeAnnotation of + Nothing -> + [] - Base64Content _ -> - CliMonad.succeed [ ( Common.UnsafeName "body", Gen.Bytes.annotation_.bytes ) ] - |> CliMonad.withRequiredPackage "elm/bytes" - |> CliMonad.withRequiredPackage Common.base64PackageName + Just ann -> + [ ( Common.UnsafeName "body", ann ) ] + ) headersFromList : (Elm.Expression -> Elm.Expression -> Elm.Expression) -> AuthorizationInfo -> Elm.Expression -> List (Elm.Expression -> ( Elm.Expression, Elm.Expression, Bool )) -> Elm.Expression headersFromList f auth config headerFunctions = @@ -1087,7 +1124,7 @@ toRequestFunctions server effectTypes method pathUrl operation = ) (case errorTypeDeclaration of Just { name, declaration, group } -> - [ { moduleName = Common.Types + [ { moduleName = Common.Types Common.Response , name = name , declaration = declaration , group = group @@ -1119,7 +1156,13 @@ toRequestFunctions server effectTypes method pathUrl operation = ) (operationToContentSchema operation) (operationToAuthorizationInfo operation) - (SchemaUtils.typeToAnnotationWithNullable successType) + (case successType of + SuccessType t -> + SchemaUtils.typeToAnnotationWithNullable t + + SuccessReference ref -> + CliMonad.refToAnnotation ref + ) in operationToTypesExpectAndResolver functionName operation |> CliMonad.andThen step @@ -1601,7 +1644,13 @@ operationToContentSchema operation = CliMonad.succeed requestOrRef |> CliMonad.stepOrFail "I found a successful response, but I couldn't convert it to a concrete one" OpenApi.Reference.toReference - |> CliMonad.map (\ref -> JsonContent (Common.Ref <| String.split "/" <| OpenApi.Reference.ref ref)) + |> CliMonad.andThen + (\raw -> + OpenApi.Reference.ref raw + |> Common.parseRequestBodyRef + |> Result.map ReferenceContent + |> CliMonad.fromResult + ) jsonRegex : Regex @@ -2086,7 +2135,7 @@ paramToString type_ = Common.Ref ref -> -- These are mostly aliases - SchemaUtils.getAlias ref + SchemaUtils.getSchema ref |> CliMonad.andThen (SchemaUtils.schemaToType []) |> CliMonad.andThen (\param -> paramToString param.type_) @@ -2150,7 +2199,7 @@ paramToString type_ = , isMaybe = False } ) - (CliMonad.moduleToNamespace Common.Types) + (CliMonad.moduleToNamespace (Common.Types Common.Schema)) _ -> SchemaUtils.typeToAnnotationWithNullable type_ @@ -2207,7 +2256,7 @@ paramToType concreteParam = case type_ of Common.Ref ref -> ref - |> SchemaUtils.getAlias + |> SchemaUtils.getSchema |> CliMonad.andThen (SchemaUtils.schemaToType []) |> CliMonad.map (\inner -> @@ -2265,7 +2314,7 @@ toConcreteParam param = type alias OperationUtils = - { successType : Common.Type + { successType : SuccessType , bodyTypeAnnotation : Elm.Annotation.Annotation , errorTypeDeclaration : Maybe { name : String, declaration : Elm.Declaration, group : String } , errorTypeAnnotation : Elm.Annotation.Annotation @@ -2277,6 +2326,11 @@ type alias OperationUtils = } +type SuccessType + = SuccessType Common.Type + | SuccessReference (Common.RefTo Common.Response) + + operationToTypesExpectAndResolver : String -> OpenApi.Operation.Operation @@ -2361,7 +2415,7 @@ operationToTypesExpectAndResolver functionName operation = JsonContent type_ -> CliMonad.map (\successDecoder -> - { successType = type_ + { successType = SuccessType type_ , bodyTypeAnnotation = Elm.Annotation.string , errorTypeDeclaration = errorTypeDeclaration_ , errorTypeAnnotation = errorTypeAnnotation @@ -2380,6 +2434,7 @@ operationToTypesExpectAndResolver functionName operation = { const = Nothing , format = Nothing } + |> SuccessType , bodyTypeAnnotation = Elm.Annotation.string , errorTypeDeclaration = errorTypeDeclaration_ , errorTypeAnnotation = errorTypeAnnotation @@ -2392,7 +2447,7 @@ operationToTypesExpectAndResolver functionName operation = |> CliMonad.succeed BytesContent _ -> - { successType = Common.Bytes + { successType = SuccessType Common.Bytes , bodyTypeAnnotation = Gen.Bytes.annotation_.bytes , errorTypeDeclaration = errorTypeDeclaration_ , errorTypeAnnotation = errorTypeAnnotation @@ -2406,7 +2461,7 @@ operationToTypesExpectAndResolver functionName operation = |> CliMonad.withRequiredPackage "elm/bytes" Base64Content _ -> - { successType = Common.Bytes + { successType = SuccessType Common.Bytes , bodyTypeAnnotation = Elm.Annotation.string , errorTypeDeclaration = errorTypeDeclaration_ , errorTypeAnnotation = errorTypeAnnotation @@ -2421,7 +2476,7 @@ operationToTypesExpectAndResolver functionName operation = |> CliMonad.withRequiredPackage Common.base64PackageName EmptyContent -> - { successType = Common.Unit + { successType = SuccessType Common.Unit , bodyTypeAnnotation = Elm.Annotation.string , errorTypeDeclaration = errorTypeDeclaration_ , errorTypeAnnotation = errorTypeAnnotation @@ -2432,6 +2487,9 @@ operationToTypesExpectAndResolver functionName operation = } } |> CliMonad.succeed + + ReferenceContent _ -> + CliMonad.fail "operationToTypesExpectAndResolver: branch 'ReferenceContent _' not implemented" ) (OpenApi.Response.content response |> contentToContentSchema @@ -2441,25 +2499,12 @@ operationToTypesExpectAndResolver functionName operation = CliMonad.succeed responseOrRef |> CliMonad.stepOrFail "I found a successful response, but I couldn't convert it to a concrete one" OpenApi.Reference.toReference + |> CliMonad.andThen parseReferenceToResponse |> CliMonad.andThen (\ref -> - let - inner : String - inner = - OpenApi.Reference.ref ref - in - CliMonad.map2 - (\importFrom typeName -> - let - decoder : Elm.Expression - decoder = - Elm.value - { importFrom = importFrom - , name = "decode" ++ Common.toTypeName typeName - , annotation = Nothing - } - in - { successType = Common.ref inner + CliMonad.map + (\decoder -> + { successType = SuccessReference ref , bodyTypeAnnotation = Elm.Annotation.string , errorTypeDeclaration = errorTypeDeclaration_ , errorTypeAnnotation = errorTypeAnnotation @@ -2470,8 +2515,7 @@ operationToTypesExpectAndResolver functionName operation = } } ) - (CliMonad.moduleToNamespace Common.Json) - (SchemaUtils.refToTypeName (String.split "/" inner)) + (CliMonad.refToDecoder ref) ) ) (errorResponsesToErrorDecoders functionName errorResponses) @@ -2479,6 +2523,17 @@ operationToTypesExpectAndResolver functionName operation = ) +parseReferenceToResponse : OpenApi.Reference.Reference -> CliMonad (Common.RefTo Common.Response) +parseReferenceToResponse ref = + let + inner : String + inner = + OpenApi.Reference.ref ref + in + Common.parseResponseRef inner + |> CliMonad.fromResult + + errorResponsesToType : String -> Dict.Dict String (OpenApi.Reference.ReferenceOr OpenApi.Response.Response) -> CliMonad ( Maybe { name : String, declaration : Elm.Declaration, group : String }, Elm.Annotation.Annotation ) errorResponsesToType functionName errorResponses = errorResponses @@ -2492,56 +2547,31 @@ errorResponsesToType functionName errorResponses = (\contentSchema -> case contentSchema of JsonContent type_ -> - CliMonad.map2 Tuple.pair - (SchemaUtils.typeToAnnotationWithNullable type_) - (SchemaUtils.typeToAnnotationWithNullable type_) + SchemaUtils.typeToAnnotationWithNullable type_ StringContent _ -> - CliMonad.succeed - ( Elm.Annotation.string - , Elm.Annotation.string - ) + CliMonad.succeed Elm.Annotation.string BytesContent _ -> - CliMonad.succeed - ( Gen.Bytes.annotation_.bytes - , Gen.Bytes.annotation_.bytes - ) + CliMonad.succeed Gen.Bytes.annotation_.bytes |> CliMonad.withRequiredPackage "elm/bytes" EmptyContent -> - CliMonad.succeed - ( Elm.Annotation.unit - , Elm.Annotation.unit - ) + CliMonad.succeed Elm.Annotation.unit Base64Content _ -> - CliMonad.succeed - ( Elm.Annotation.string - , Elm.Annotation.string - ) + CliMonad.succeed Elm.Annotation.string + + ReferenceContent ref -> + CliMonad.refToAnnotation ref ) Nothing -> CliMonad.succeed errResponseOrRef |> CliMonad.stepOrFail "I found an error response, but I couldn't convert it to a concrete annotation" OpenApi.Reference.toReference - |> CliMonad.andThen2 - (\importFrom ref -> - let - inner : String - inner = - OpenApi.Reference.ref ref - in - SchemaUtils.refToTypeName (String.split "/" inner) - |> CliMonad.map - (\typeName -> - ( Elm.Annotation.named [] (Common.toTypeName typeName) - , Elm.Annotation.named importFrom (Common.toTypeName typeName) - ) - ) - ) - (CliMonad.moduleToNamespace Common.Types) + |> CliMonad.andThen parseReferenceToResponse + |> CliMonad.andThen CliMonad.refToAnnotation ) |> CliMonad.combineDict |> CliMonad.andThen @@ -2551,8 +2581,8 @@ errorResponsesToType functionName errorResponses = ( Nothing, Elm.Annotation.var "e" ) |> CliMonad.succeed - [ ( _, ( _, globalAnnotation ) ) ] -> - ( Nothing, globalAnnotation ) + [ ( _, annotation ) ] -> + ( Nothing, annotation ) |> CliMonad.succeed errorList -> @@ -2561,13 +2591,13 @@ errorResponsesToType functionName errorResponses = errorName = String.Extra.toSentenceCase functionName ++ "_Error" in - CliMonad.moduleToNamespace Common.Types + CliMonad.moduleToNamespace (Common.Types Common.Response) |> CliMonad.map (\importFrom -> ( { name = errorName , declaration = errorList - |> List.map (\( statusCode, ( localAnnotation, _ ) ) -> Elm.variantWith (toErrorVariant functionName statusCode) [ localAnnotation ]) + |> List.map (\( statusCode, annotation ) -> Elm.variantWith (toErrorVariant functionName statusCode) [ annotation ]) |> Elm.customType errorName |> Elm.exposeConstructor , group = "Errors" @@ -2598,7 +2628,7 @@ errorResponsesToErrorDecoders functionName errorResponses = _ -> False in - CliMonad.moduleToNamespace Common.Types + CliMonad.moduleToNamespace (Common.Types Common.Response) |> CliMonad.andThen (\typesNamespace -> errorList @@ -2628,6 +2658,9 @@ errorResponsesToErrorDecoders functionName errorResponses = EmptyContent -> CliMonad.succeed (Gen.Json.Decode.succeed Elm.unit) + + ReferenceContent _ -> + CliMonad.todo "$ref errors are not supported yet" ) Nothing -> @@ -2641,16 +2674,9 @@ errorResponsesToErrorDecoders functionName errorResponses = inner = OpenApi.Reference.ref ref in - CliMonad.map2 - (\jsonNamespace typeName -> - Elm.value - { importFrom = jsonNamespace - , name = "decode" ++ Common.toTypeName typeName - , annotation = Nothing - } - ) - (CliMonad.moduleToNamespace Common.Json) - (SchemaUtils.refToTypeName (String.split "/" inner)) + Common.parseRef inner + |> CliMonad.fromResult + |> CliMonad.andThen CliMonad.refToDecoder ) in decoder diff --git a/src/SchemaUtils.elm b/src/SchemaUtils.elm index 63b245e7..d9f0417d 100644 --- a/src/SchemaUtils.elm +++ b/src/SchemaUtils.elm @@ -1,9 +1,8 @@ module SchemaUtils exposing ( OneOfName - , getAlias + , getSchema , oneOfDeclarations , recordType - , refToTypeName , schemaToType , subschemaToEnumMaybe , toVariantName @@ -52,8 +51,12 @@ import Result.Extra import Set exposing (Set) -getSchema : String -> CliMonad Json.Schema.Definitions.Schema -getSchema refName = +getSchema : Common.RefTo Common.Schema -> CliMonad Json.Schema.Definitions.Schema +getSchema ref = + let + ( _, Common.UnsafeName refName ) = + Common.unwrapRef ref + in CliMonad.getApiSpec |> CliMonad.stepOrFail ("Could not find components in the schema, while looking up " ++ refName) OpenApi.components @@ -62,17 +65,7 @@ getSchema refName = |> CliMonad.map OpenApi.Schema.get -getAlias : List String -> CliMonad Json.Schema.Definitions.Schema -getAlias refUri = - case refUri of - [ "#", "components", _, refName ] -> - getSchema refName - - _ -> - CliMonad.fail <| "Couldn't get the type ref (" ++ String.join "/" refUri ++ ") for the response" - - -subSchemaAllOfToProperties : List (List String) -> Json.Schema.Definitions.SubSchema -> CliMonad (List ( Common.UnsafeName, Common.Field )) +subSchemaAllOfToProperties : List (Common.RefTo Common.Schema) -> Json.Schema.Definitions.SubSchema -> CliMonad (List ( Common.UnsafeName, Common.Field )) subSchemaAllOfToProperties seen subSchema = subSchema.allOf |> Maybe.withDefault [] @@ -81,7 +74,7 @@ subSchemaAllOfToProperties seen subSchema = |> CliMonad.withPath (Common.UnsafeName "allOf") -schemaToProperties : List (List String) -> Json.Schema.Definitions.Schema -> CliMonad (List ( Common.UnsafeName, Common.Field )) +schemaToProperties : List (Common.RefTo Common.Schema) -> Json.Schema.Definitions.Schema -> CliMonad (List ( Common.UnsafeName, Common.Field )) schemaToProperties seen allOfItem = case allOfItem of Json.Schema.Definitions.ObjectSchema allOfItemSchema -> @@ -110,19 +103,21 @@ listUnion l r = |> List.map (Tuple.mapFirst Common.UnsafeName) -subSchemaRefToProperties : List (List String) -> Json.Schema.Definitions.SubSchema -> CliMonad (List ( Common.UnsafeName, Common.Field )) +subSchemaRefToProperties : List (Common.RefTo Common.Schema) -> Json.Schema.Definitions.SubSchema -> CliMonad (List ( Common.UnsafeName, Common.Field )) subSchemaRefToProperties seen allOfItem = case allOfItem.ref of Nothing -> CliMonad.succeed [] Just ref -> - getAlias (String.split "/" ref) + Common.parseSchemaRef ref + |> CliMonad.fromResult + |> CliMonad.andThen getSchema |> CliMonad.andThen (schemaToProperties seen) |> CliMonad.withPath (Common.UnsafeName ref) -subSchemaToProperties : List (List String) -> Json.Schema.Definitions.SubSchema -> CliMonad (List ( Common.UnsafeName, Common.Field )) +subSchemaToProperties : List (Common.RefTo Common.Schema) -> Json.Schema.Definitions.SubSchema -> CliMonad (List ( Common.UnsafeName, Common.Field )) subSchemaToProperties seen sch = -- TODO: rename let @@ -151,7 +146,7 @@ subSchemaToProperties seen sch = ) -schemaToType : List (List String) -> Json.Schema.Definitions.Schema -> CliMonad { type_ : Common.Type, documentation : Maybe String } +schemaToType : List (Common.RefTo Common.Schema) -> Json.Schema.Definitions.Schema -> CliMonad { type_ : Common.Type, documentation : Maybe String } schemaToType seen schema = case schema of Json.Schema.Definitions.BooleanSchema _ -> @@ -391,7 +386,14 @@ schemaToType seen schema = Json.Schema.Definitions.AnyType -> case subSchema.ref of Just ref -> - CliMonad.succeed { type_ = Common.ref ref, documentation = subSchema.description } + Common.parseSchemaRef ref + |> CliMonad.fromResult + |> CliMonad.map + (\parsed -> + { type_ = Common.Ref parsed + , documentation = subSchema.description + } + ) Nothing -> case subSchema.anyOf of @@ -546,7 +548,7 @@ type SchemaIntersectionResult } -schemaIntersection : List (List String) -> List Json.Schema.Definitions.Schema -> CliMonad SchemaIntersectionResult +schemaIntersection : List (Common.RefTo Common.Schema) -> List Json.Schema.Definitions.Schema -> CliMonad SchemaIntersectionResult schemaIntersection seen schemas = let intersect : Json.Schema.Definitions.Schema -> Json.Schema.Definitions.Schema -> CliMonad SchemaIntersectionResult @@ -654,21 +656,27 @@ describeSubSchema subSchema = CliMonad.succeed (sourceToString subSchema.source) Just ref -> - getAlias (String.split "/" ref) - |> CliMonad.map - (\f -> - let - source : Json.Decode.Value - source = - case f of - Json.Schema.Definitions.ObjectSchema o -> - o.source - - Json.Schema.Definitions.BooleanSchema b -> - Json.Encode.bool b - in - Pretty.string (ref ++ ": ") - |> Pretty.a (sourceToString source) + Common.parseSchemaRef ref + |> CliMonad.fromResult + |> CliMonad.andThen + (\parsedRef -> + getSchema parsedRef + |> CliMonad.map + (\f -> + let + source : Json.Decode.Value + source = + case f of + Json.Schema.Definitions.ObjectSchema o -> + o.source + + Json.Schema.Definitions.BooleanSchema b -> + Json.Encode.bool b + in + Pretty.string (ref ++ ": ") + |> Pretty.a (sourceToString source) + ) + |> CliMonad.withPath (Common.refToString parsedRef) ) @@ -692,7 +700,7 @@ removeDocumentation = ) -exampleOfType : List (List String) -> Common.Type -> CliMonad Json.Encode.Value +exampleOfType : List (Common.RefTo Common.Schema) -> Common.Type -> CliMonad Json.Encode.Value exampleOfType seen type_ = case type_ of Common.Nullable _ -> @@ -799,13 +807,13 @@ exampleOfType seen type_ = else let - newSeen : List (List String) + newSeen : List (Common.RefTo Common.Schema) newSeen = ref :: seen in refToType newSeen ref |> CliMonad.andThen (\t -> exampleOfType newSeen t) - |> CliMonad.withPath (Common.UnsafeName (String.join "/" ref)) + |> CliMonad.withPath (Common.refToString ref) Common.Bytes -> Json.Encode.string "" @@ -816,11 +824,11 @@ exampleOfType seen type_ = |> CliMonad.succeed -refToType : List (List String) -> List String -> CliMonad Common.Type +refToType : List (Common.RefTo Common.Schema) -> Common.RefTo Common.Schema -> CliMonad Common.Type refToType seen ref = CliMonad.getOrCache ref (\() -> - getAlias ref + getSchema ref |> CliMonad.andThen (schemaToType seen) |> CliMonad.map .type_ ) @@ -837,23 +845,23 @@ type SimplifiedForIntersectionBasicType | SimplifiedForIntersectionBool (Maybe Bool) -typesIntersection : List (List String) -> Common.Type -> Common.Type -> CliMonad (IntersectionResult Json.Encode.Value) +typesIntersection : List (Common.RefTo Common.Schema) -> Common.Type -> Common.Type -> CliMonad (IntersectionResult Json.Encode.Value) typesIntersection seen lType rType = let - followRef : List String -> Common.Type -> CliMonad (IntersectionResult Json.Encode.Value) + followRef : Common.RefTo Common.Schema -> Common.Type -> CliMonad (IntersectionResult Json.Encode.Value) followRef ref oType = if List.Extra.count ((==) ref) seen > 2 then CliMonad.succeed IntersectionResult.MayIntersect else let - newSeen : List (List String) + newSeen : List (Common.RefTo Common.Schema) newSeen = ref :: seen in refToType newSeen ref |> CliMonad.andThen (\type_ -> typesIntersection newSeen type_ oType) - |> CliMonad.withPath (Common.UnsafeName (String.join "/" ref)) + |> CliMonad.withPath (Common.refToString ref) in case ( lType, rType ) of ( Common.Ref lRef, Common.Ref rRef ) -> @@ -1031,7 +1039,7 @@ typesIntersection seen lType rType = |> CliMonad.withWarning ("Disjoint check not implemented for types " ++ typeToString lType ++ " and " ++ typeToString rType) -typesIntersectionOneOf : List (List String) -> List Common.OneOfData -> Common.Type -> CliMonad (IntersectionResult Json.Encode.Value) +typesIntersectionOneOf : List (Common.RefTo Common.Schema) -> List Common.OneOfData -> Common.Type -> CliMonad (IntersectionResult Json.Encode.Value) typesIntersectionOneOf seen alternatives t = alternatives |> CliMonad.combineMap @@ -1070,7 +1078,7 @@ findAnyIntersection = objectsIntersection : - List (List String) + List (Common.RefTo Common.Schema) -> ( Common.Object, Maybe Common.Type ) -> ( Common.Object, Maybe Common.Type ) -> CliMonad (IntersectionResult Json.Encode.Value) @@ -1382,16 +1390,11 @@ oneOfType types = Just variants -> let sortedVariants : - ( { name : Common.UnsafeName - , type_ : Common.Type - , documentation : Maybe String - } - , List + NonEmpty.NonEmpty { name : Common.UnsafeName , type_ : Common.Type , documentation : Maybe String } - ) sortedVariants = NonEmpty.sortBy (\{ name } -> Common.unwrapUnsafe name) variants @@ -1426,7 +1429,7 @@ oneOfType types = {-| Transform an object schema's named and inherited (via $ref) properties to a type -} -objectSchemaToTypeHelp : List (List String) -> Json.Schema.Definitions.SubSchema -> CliMonad { type_ : Common.Type, documentation : Maybe String } +objectSchemaToTypeHelp : List (Common.RefTo Common.Schema) -> Json.Schema.Definitions.SubSchema -> CliMonad { type_ : Common.Type, documentation : Maybe String } objectSchemaToTypeHelp seen subSchema = CliMonad.map2 (\schemaProps allOfProps -> @@ -1467,7 +1470,7 @@ objectSchemaToTypeHelp seen subSchema = (subSchemaAllOfToProperties seen subSchema) -objectSchemaToType : List (List String) -> Json.Schema.Definitions.SubSchema -> CliMonad { type_ : Common.Type, documentation : Maybe String } +objectSchemaToType : List (Common.RefTo Common.Schema) -> Json.Schema.Definitions.SubSchema -> CliMonad { type_ : Common.Type, documentation : Maybe String } objectSchemaToType seen subSchema = let declaredProperties : CliMonad { type_ : Common.Type, documentation : Maybe String } @@ -1599,7 +1602,7 @@ oneOfDeclaration ( oneOfName, variants ) = |> CliMonad.combineMap variantDeclaration |> CliMonad.map (\decl -> - { moduleName = Common.Types + { moduleName = Common.Types Common.Schema , name = oneOfName , group = "One of" , declaration = @@ -1683,7 +1686,7 @@ typeToAnnotationWithNullable type_ = CliMonad.succeed Elm.Annotation.string Just name -> - nameToAnnotation name + CliMonad.nameToAnnotation Common.Schema name ) Common.OneOf oneOfName oneOfData -> @@ -1693,12 +1696,7 @@ typeToAnnotationWithNullable type_ = CliMonad.succeed Gen.Json.Encode.annotation_.value Common.Ref ref -> - refToTypeName ref - |> CliMonad.andThen - (\name -> - nameToAnnotation name - ) - |> CliMonad.withPath (Common.UnsafeName (String.join "/" ref)) + CliMonad.refToAnnotation ref Common.Bytes -> CliMonad.succeed Gen.Bytes.annotation_.bytes @@ -1763,7 +1761,7 @@ typeToAnnotationWithMaybe type_ = CliMonad.succeed Elm.Annotation.string Just name -> - nameToAnnotation name + CliMonad.nameToAnnotation Common.Schema name ) Common.OneOf oneOfName oneOfData -> @@ -1773,10 +1771,7 @@ typeToAnnotationWithMaybe type_ = CliMonad.succeed Gen.Json.Encode.annotation_.value Common.Ref ref -> - refToTypeName ref - |> CliMonad.andThen - (\name -> nameToAnnotation name) - |> CliMonad.withPath (Common.UnsafeName (String.join "/" ref)) + CliMonad.refToAnnotation ref Common.Bytes -> CliMonad.succeed Gen.Bytes.annotation_.bytes @@ -1786,17 +1781,6 @@ typeToAnnotationWithMaybe type_ = CliMonad.succeed Elm.Annotation.unit -nameToAnnotation : Common.UnsafeName -> CliMonad Elm.Annotation.Annotation -nameToAnnotation name = - CliMonad.moduleToNamespace Common.Types - |> CliMonad.map - (\importFrom -> - Elm.Annotation.named - importFrom - (Common.toTypeName name) - ) - - basicTypeToAnnotation : Common.BasicType -> { a | format : Maybe String } -> CliMonad Elm.Annotation.Annotation basicTypeToAnnotation basicType { format } = let @@ -1875,18 +1859,7 @@ typeToEncoder type_ = CliMonad.succeed Gen.Json.Encode.call_.string Just name -> - CliMonad.map - (\importFrom rec -> - Elm.apply - (Elm.value - { importFrom = importFrom - , name = "encode" ++ Common.toTypeName name - , annotation = Nothing - } - ) - [ rec ] - ) - (CliMonad.moduleToNamespace Common.Json) + CliMonad.refToEncoder (Common.refTo Common.Schema name) ) Common.Object properties -> @@ -2029,20 +2002,7 @@ typeToEncoder type_ = CliMonad.succeed <| Gen.Basics.identity Common.Ref ref -> - refToTypeName ref - |> CliMonad.map2 - (\importFrom name rec -> - Elm.apply - (Elm.value - { importFrom = importFrom - , name = "encode" ++ Common.toTypeName name - , annotation = Nothing - } - ) - [ rec ] - ) - (CliMonad.moduleToNamespace Common.Json) - |> CliMonad.withPath (Common.UnsafeName (String.join "/" ref)) + CliMonad.refToEncoder ref Common.OneOf oneOfName oneOfData -> oneOfData @@ -2067,7 +2027,7 @@ typeToEncoder type_ = (Elm.Annotation.named importFrom oneOfName) branches ) - (CliMonad.moduleToNamespace Common.Types) + (CliMonad.moduleToNamespace (Common.Types Common.Schema)) Common.Bytes -> CliMonad.todo "Encoder for bytes not implemented" @@ -2100,7 +2060,7 @@ basicTypeToEncoder basicType { format } = oneOfAnnotation : Common.TypeName -> List Common.OneOfData -> CliMonad Elm.Annotation.Annotation oneOfAnnotation oneOfName oneOfData = - CliMonad.moduleToNamespace Common.Types + CliMonad.moduleToNamespace (Common.Types Common.Schema) |> CliMonad.andThen (\importFrom -> Elm.Annotation.named @@ -2111,16 +2071,6 @@ oneOfAnnotation oneOfName oneOfData = ) -refToTypeName : List String -> CliMonad Common.UnsafeName -refToTypeName ref = - case ref of - [ "#", "components", _, name ] -> - CliMonad.succeed (Common.UnsafeName name) - - _ -> - CliMonad.fail <| "Couldn't get the type ref (" ++ String.join "/" ref ++ ") for the response" - - typeToDecoder : Common.Type -> CliMonad Elm.Expression typeToDecoder type_ = case type_ of @@ -2364,15 +2314,7 @@ typeToDecoder type_ = |> CliMonad.succeed Just name -> - CliMonad.map - (\importFrom -> - Elm.value - { importFrom = importFrom - , name = "decode" ++ Common.toTypeName name - , annotation = Nothing - } - ) - (CliMonad.moduleToNamespace Common.Json) + CliMonad.refToDecoder (Common.refTo Common.Schema name) ) Common.Value -> @@ -2392,17 +2334,7 @@ typeToDecoder type_ = (typeToDecoder t) Common.Ref ref -> - CliMonad.map2 - (\importFrom name -> - Elm.value - { importFrom = importFrom - , name = "decode" ++ Common.toTypeName name - , annotation = Nothing - } - ) - (CliMonad.moduleToNamespace Common.Json) - (refToTypeName ref) - |> CliMonad.withPath (Common.UnsafeName (String.join "/" ref)) + CliMonad.refToDecoder ref Common.OneOf oneOfName variants -> variants @@ -2420,7 +2352,7 @@ typeToDecoder type_ = } ) ) - (CliMonad.moduleToNamespace Common.Types) + (CliMonad.moduleToNamespace (Common.Types Common.Schema)) |> CliMonad.withPath variant.name ) |> CliMonad.map2 @@ -2429,7 +2361,7 @@ typeToDecoder type_ = |> Gen.Json.Decode.oneOf |> Elm.withType (Elm.Annotation.named importFrom oneOfName) ) - (CliMonad.moduleToNamespace Common.Types) + (CliMonad.moduleToNamespace (Common.Types Common.Schema)) Common.Bytes -> CliMonad.todo "Bytes decoder not implemented yet" diff --git a/tests/Test/OpenApi/Generate.elm b/tests/Test/OpenApi/Generate.elm index d2ad49c2..2fe60c42 100644 --- a/tests/Test/OpenApi/Generate.elm +++ b/tests/Test/OpenApi/Generate.elm @@ -1,18 +1,26 @@ -module Test.OpenApi.Generate exposing (suite) +module Test.OpenApi.Generate exposing (fuzzInputName, fuzzTitle, issue48, pr267) +import Ansi.Color import CliMonad +import Dict +import Dict.Extra +import Diff +import Diff.ToString import Elm import Expect import FastDict import FastSet import Fuzz import Json.Decode +import Json.Encode import OpenApi import OpenApi.Config import OpenApi.Generate import String.Extra +import String.Multiline import Test exposing (Test) import Utils +import Yaml.Decode moduleNames : List { a | moduleName : List String } -> String @@ -40,40 +48,43 @@ composeExpectations expectations = () -suite : Test -suite = - Test.describe "open api generation" - [ Test.fuzz Fuzz.string - "sanitizeModuleName" - <| - \inputName -> - let - moduleName : Maybe String - moduleName = - Utils.sanitizeModuleName inputName - in - if String.any Char.isAlphaNum inputName then - case moduleName of - Just finalName -> - if String.all Char.isAlphaNum finalName then - Expect.pass - - else - Expect.fail ("Unexpected chars in module name: " ++ finalName) - - Nothing -> - Expect.fail "Unexpected Nothing" - - else - Expect.equal moduleName Nothing - , Test.fuzz Fuzz.string - "valid output file & module name" - <| - \inputName -> - let - oasString : String - oasString = - """{ +fuzzInputName : Test +fuzzInputName = + Test.fuzz Fuzz.string + "sanitizeModuleName" + <| + \inputName -> + let + moduleName : Maybe String + moduleName = + Utils.sanitizeModuleName inputName + in + if String.any Char.isAlphaNum inputName then + case moduleName of + Just finalName -> + if String.all Char.isAlphaNum finalName then + Expect.pass + + else + Expect.fail ("Unexpected chars in module name: " ++ finalName) + + Nothing -> + Expect.fail "Unexpected Nothing" + + else + Expect.equal moduleName Nothing + + +fuzzTitle : Test +fuzzTitle = + Test.fuzz Fuzz.string + "valid output file & module name" + <| + \inputName -> + let + oasString : String + oasString = + """{ "openapi": "3.1.0", "info": { "title": \"""" ++ inputName ++ """", @@ -86,85 +97,436 @@ suite = } } }""" - in - case Json.Decode.decodeString OpenApi.decode oasString of - Err _ -> - Expect.pass - - Ok oas -> - let - namespace : List String - namespace = - Utils.sanitizeModuleName inputName - |> Maybe.withDefault "Carl" - |> List.singleton - - genFiles : - Result - CliMonad.Message - { modules : - List - { moduleName : List String - , declarations : FastDict.Dict String { group : String, declaration : Elm.Declaration } - } - , warnings : List CliMonad.Message - , requiredPackages : FastSet.Set String - } - genFiles = - OpenApi.Generate.files - { namespace = namespace - , generateTodos = False - , effectTypes = [ OpenApi.Config.ElmHttpCmd, OpenApi.Config.ElmHttpTask ] - , server = OpenApi.Config.Default - , formats = OpenApi.Config.defaultFormats - , warnOnMissingEnums = True - } - oas - in - case genFiles of - Err _ -> - Expect.fail "Unexpected generation error" - - Ok { modules } -> - case modules of - [ jsonFile, apiFile, helperFile ] -> - let - jsonPath : List String - jsonPath = - namespace ++ [ "Json" ] - - apiPath : List String - apiPath = - namespace ++ [ "Api" ] - - helperPath : List String - helperPath = - namespace ++ [ "OpenApi" ] - in - composeExpectations - [ expectModuleName jsonFile jsonPath - , expectModuleName apiFile apiPath - , expectModuleName helperFile helperPath - ] - - [] -> - Expect.fail "Expected to generate 3 files but found none" - - _ -> - Expect.fail - ("Expected to generate 3 files but found " - ++ (List.length modules |> String.fromInt) - ++ ": " - ++ moduleNames modules - ) - - -- Known bug: https://github.com/wolfadex/elm-open-api-cli/issues/48 - , Test.test "The OAS title: service API (params in:body)" <| - \() -> - let - moduleName : Maybe String - moduleName = - Utils.sanitizeModuleName "service API (params in:body)" - in - Expect.equal moduleName (Just "ServiceApiParamsInBody") + in + case Json.Decode.decodeString OpenApi.decode oasString of + Err _ -> + Expect.pass + + Ok oas -> + let + namespace : List String + namespace = + Utils.sanitizeModuleName inputName + |> Maybe.withDefault "Carl" + |> List.singleton + + genFiles : + Result + CliMonad.Message + { modules : + List + { moduleName : List String + , declarations : FastDict.Dict String { group : String, declaration : Elm.Declaration } + } + , warnings : List CliMonad.Message + , requiredPackages : FastSet.Set String + } + genFiles = + OpenApi.Generate.files + { namespace = namespace + , generateTodos = False + , effectTypes = [ OpenApi.Config.ElmHttpCmd, OpenApi.Config.ElmHttpTask ] + , server = OpenApi.Config.Default + , formats = OpenApi.Config.defaultFormats + , warnOnMissingEnums = True + } + oas + in + case genFiles of + Err e -> + Expect.fail ("Error in generation: " ++ Debug.toString e) + + Ok { modules } -> + case modules of + [ jsonFile, apiFile, helperFile ] -> + let + jsonPath : List String + jsonPath = + namespace ++ [ "Json" ] + + apiPath : List String + apiPath = + namespace ++ [ "Api" ] + + helperPath : List String + helperPath = + namespace ++ [ "OpenApi" ] + in + composeExpectations + [ expectModuleName jsonFile jsonPath + , expectModuleName apiFile apiPath + , expectModuleName helperFile helperPath + ] + + [] -> + Expect.fail "Expected to generate 3 files but found none" + + _ -> + Expect.fail + ("Expected to generate 3 files but found " + ++ (List.length modules |> String.fromInt) + ++ ": " + ++ moduleNames modules + ) + + +{-| Known bug: +-} +issue48 : Test +issue48 = + Test.test "The OAS title: service API (params in:body)" <| + \() -> + let + moduleName : Maybe String + moduleName = + Utils.sanitizeModuleName "service API (params in:body)" + in + Expect.equal moduleName (Just "ServiceApiParamsInBody") + + +pr267 : Test +pr267 = + Test.test "Responses and schemas don't clash" <| + \() -> + let + oasString : String + oasString = + String.Multiline.here """ + openapi: 3.0.1 + info: + title: "Simple Ref" + version: 1.0.0 + components: + schemas: + Forbidden: + $ref: "#/components/schemas/Message" + Message: + type: object + properties: + msg: + type: string + required: + - msg + responses: + Forbidden: + description: Wrong credentials + content: + application/json: + schema: + $ref: "#/components/schemas/Forbidden" + paths: + "/api": + get: + responses: + 200: + $ref: "#/components/responses/Forbidden" + """ + in + case + oasString + |> Yaml.Decode.fromString yamlToJsonValueDecoder + |> Result.mapError Debug.toString + |> Result.andThen + (\json -> + json + |> Json.Decode.decodeValue OpenApi.decode + |> Result.mapError Debug.toString + ) + of + Err e -> + Expect.fail e + + Ok oas -> + let + genFiles : + Result + CliMonad.Message + { modules : + List + { moduleName : List String + , declarations : FastDict.Dict String { group : String, declaration : Elm.Declaration } + } + , warnings : List CliMonad.Message + , requiredPackages : FastSet.Set String + } + genFiles = + OpenApi.Generate.files + { namespace = [ "Output" ] + , generateTodos = False + , effectTypes = [ OpenApi.Config.ElmHttpCmd, OpenApi.Config.ElmHttpTask ] + , server = OpenApi.Config.Default + , formats = OpenApi.Config.defaultFormats + , warnOnMissingEnums = True + } + oas + in + case genFiles of + Err e -> + Expect.fail ("Error in generation: " ++ Debug.toString e) + + Ok { modules } -> + case modules of + [ apiFile, jsonFile, jsonResponsesFile, typesFile, typesResponsesFile ] -> + let + jsonFileString : String + jsonFileString = + String.Multiline.here """ + module Output.Json exposing + ( decodeForbidden, decodeMessage + , encodeForbidden, encodeMessage + ) + + {-| + @docs decodeForbidden, decodeMessage + + @docs encodeForbidden, encodeMessage + -} + + + import Json.Decode + import Json.Encode + import OpenApi.Common + import Output.Types + + + {- ## Decoders -} + + + decodeForbidden : Json.Decode.Decoder Output.Types.Forbidden + decodeForbidden = + decodeMessage + + + decodeMessage : Json.Decode.Decoder Output.Types.Message + decodeMessage = + Json.Decode.succeed + (\\msg -> { msg = msg }) |> OpenApi.Common.jsonDecodeAndMap + (Json.Decode.field + "msg" + Json.Decode.string + ) + + + {- ## Encoders -} + + + encodeForbidden : Output.Types.Forbidden -> Json.Encode.Value + encodeForbidden = + encodeMessage + + + encodeMessage : Output.Types.Message -> Json.Encode.Value + encodeMessage rec = + Json.Encode.object [ ( "msg", Json.Encode.string rec.msg ) ] + """ + + jsonResponsesFileString : String + jsonResponsesFileString = + String.Multiline.here """ + module Output.Json.Responses exposing + ( decodeForbidden + , encodeForbidden + ) + + {-| + @docs decodeForbidden + + @docs encodeForbidden + -} + + + import Json.Decode + import Json.Encode + import Output.Json + import Output.Types + import Output.Types.Responses + + + {- ## Decoders -} + + + decodeForbidden : Json.Decode.Decoder Output.Types.Responses.Forbidden + decodeForbidden = + Output.Json.decodeForbidden + + + {- ## Encoders -} + + + encodeForbidden : Output.Types.Responses.Forbidden -> Json.Encode.Value + encodeForbidden = + Output.Json.encodeForbidden + """ + + apiFileString : String + apiFileString = + String.Multiline.here """ + module Output.Api exposing ( api, apiTask ) + + {-| + @docs api, apiTask + -} + + + import Dict + import Http + import Json.Decode + import OpenApi.Common + import Output.Json.Responses + import Output.Types.Responses + import Task + import Url.Builder + + + {- ## Operations -} + + + api config = + Http.request + { url = Url.Builder.absolute [ "api" ] [] + , method = "GET" + , headers = [] + , expect = + OpenApi.Common.expectJsonCustom + (Dict.fromList []) + Output.Json.Responses.decodeForbidden + config.toMsg + , body = Http.emptyBody + , timeout = Nothing + , tracker = Nothing + } + + + apiTask : + {} + -> Task.Task (OpenApi.Common.Error e String) Output.Types.Responses.Forbidden + apiTask config = + Http.task + { url = Url.Builder.absolute [ "api" ] [] + , method = "GET" + , headers = [] + , resolver = + OpenApi.Common.jsonResolverCustom + (Dict.fromList []) + Output.Json.Responses.decodeForbidden + , body = Http.emptyBody + , timeout = Nothing + } + """ + + typesFileString : String + typesFileString = + String.Multiline.here """ + module Output.Types exposing ( Forbidden, Message ) + + {-| + @docs Forbidden, Message + -} + + + + {- ## Aliases -} + + + type alias Forbidden = + Message + + + type alias Message = + { msg : String } + """ + + typesResponsesFileString : String + typesResponsesFileString = + String.Multiline.here """ + module Output.Types.Responses exposing ( Forbidden ) + + {-| + @docs Forbidden + -} + + + import Output.Types + + + {- ## Aliases -} + + + type alias Forbidden = + Output.Types.Forbidden + """ + in + composeExpectations + [ expectEqualMultiline apiFileString (fileToString apiFile) + , expectEqualMultiline jsonFileString (fileToString jsonFile) + , expectEqualMultiline jsonResponsesFileString (fileToString jsonResponsesFile) + , expectEqualMultiline typesFileString (fileToString typesFile) + , expectEqualMultiline typesResponsesFileString (fileToString typesResponsesFile) + ] + + [] -> + Expect.fail "Expected to generate 3 files but found none" + + _ -> + Expect.fail + ("Expected to generate 3 files but found " + ++ (List.length modules |> String.fromInt) + ++ ": " + ++ moduleNames modules + ) + + +yamlToJsonValueDecoder : Yaml.Decode.Decoder Json.Encode.Value +yamlToJsonValueDecoder = + Yaml.Decode.oneOf + [ Yaml.Decode.map Json.Encode.float Yaml.Decode.float + , Yaml.Decode.map (\_ -> Json.Encode.null) Yaml.Decode.null + , Yaml.Decode.map Json.Encode.string Yaml.Decode.string + , Yaml.Decode.map Json.Encode.bool Yaml.Decode.bool + , Yaml.Decode.map + (Json.Encode.list identity) + (Yaml.Decode.list (Yaml.Decode.lazy (\_ -> yamlToJsonValueDecoder))) + , Yaml.Decode.map + (\dict -> Json.Encode.object (Dict.toList dict)) + (Yaml.Decode.dict (Yaml.Decode.lazy (\_ -> yamlToJsonValueDecoder))) ] + + +fileToString : { moduleName : List String, declarations : FastDict.Dict String { group : String, declaration : Elm.Declaration } } -> String +fileToString file = + file.declarations + |> FastDict.toList + |> Dict.Extra.groupBy (\( _, { group } ) -> group) + |> Dict.toList + |> List.map + (\( group, decls ) -> + Elm.group + (Elm.comment ("## " ++ group) :: List.map (\( _, { declaration } ) -> declaration) decls) + ) + |> Elm.file file.moduleName + |> .contents + + +expectEqualMultiline : String -> String -> Expect.Expectation +expectEqualMultiline exp actual = + if exp == actual then + Expect.pass + + else + let + header : String + header = + Ansi.Color.fontColor Ansi.Color.blue "Diff from expected to actual:" + in + Expect.fail + (header + ++ "\n" + ++ (Diff.diffLinesWith + (Diff.defaultOptions + |> Diff.ignoreLeadingWhitespace + ) + exp + actual + |> Diff.ToString.diffToString { context = 4, color = True } + ) + )