diff --git a/cli/example/path-level-params.yaml b/cli/example/path-level-params.yaml new file mode 100644 index 00000000..cbb8d569 --- /dev/null +++ b/cli/example/path-level-params.yaml @@ -0,0 +1,41 @@ +openapi: "3.1.0" +info: + title: "Path Level Params Test" + version: "1.0.0" +paths: + /orgs/{orgId}/teams/{teamId}/items: + parameters: + - $ref: '#/components/parameters/orgIdParam' + - in: path + name: teamId + required: true + schema: + type: string + get: + operationId: getItems + parameters: + - in: query + name: status + required: false + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + count: + type: integer + required: + - count +components: + parameters: + orgIdParam: + in: path + name: orgId + required: true + schema: + type: string diff --git a/cli/src/TestGenScript.elm b/cli/src/TestGenScript.elm index 4944fad4..4481dd23 100644 --- a/cli/src/TestGenScript.elm +++ b/cli/src/TestGenScript.elm @@ -39,10 +39,23 @@ run = binaryResponse = OpenApi.Config.inputFrom (OpenApi.Config.File "./example/binary-response.yaml") + bug : Int -> OpenApi.Config.Input + bug n = + OpenApi.Config.inputFrom (OpenApi.Config.File ("./example/openapi-generator-bugs/" ++ String.fromInt n ++ ".yaml")) + cookieAuth : OpenApi.Config.Input cookieAuth = OpenApi.Config.inputFrom (OpenApi.Config.File "./example/cookie-auth.yaml") + dbFahrplanApi : OpenApi.Config.Input + dbFahrplanApi = + OpenApi.Config.inputFrom (OpenApi.Config.File "./example/db-fahrplan-api-specification.yaml") + + gitHub : OpenApi.Config.Input + gitHub = + OpenApi.Config.inputFrom (OpenApi.Config.File "./example/github-spec.json") + |> OpenApi.Config.withWarnOnMissingEnums False + ifconfigOvh : OpenApi.Config.Input ifconfigOvh = OpenApi.Config.inputFrom (OpenApi.Config.File "./example/ifconfig.ovh.json") @@ -60,6 +73,14 @@ run = OpenApi.Config.inputFrom (OpenApi.Config.File "./example/overriding-global-security.yaml") |> OpenApi.Config.withOverrides [ OpenApi.Config.File "./example/overriding-global-security-override.yaml" ] + pathLevelParams : OpenApi.Config.Input + pathLevelParams = + OpenApi.Config.inputFrom (OpenApi.Config.File "./example/path-level-params.yaml") + + patreon : OpenApi.Config.Input + patreon = + OpenApi.Config.inputFrom (OpenApi.Config.File "./example/patreon.json") + realworldConduit : OpenApi.Config.Input realworldConduit = OpenApi.Config.inputFrom (OpenApi.Config.File "./example/realworld-conduit.yaml") @@ -76,9 +97,9 @@ run = singleEnum = OpenApi.Config.inputFrom (OpenApi.Config.File "./example/single-enum.yaml") - uuidArrayParam : OpenApi.Config.Input - uuidArrayParam = - OpenApi.Config.inputFrom (OpenApi.Config.File "./example/uuid-array-param.yaml") + telegramBot : OpenApi.Config.Input + telegramBot = + OpenApi.Config.inputFrom (OpenApi.Config.File "./example/telegram-bot.json") trustmark : OpenApi.Config.Input trustmark = @@ -93,31 +114,14 @@ run = |> OpenApi.Config.withOutputModuleName [ "Trustmark", "TradeCheck" ] |> OpenApi.Config.withEffectTypes [ OpenApi.Config.ElmHttpCmd ] + uuidArrayParam : OpenApi.Config.Input + uuidArrayParam = + OpenApi.Config.inputFrom (OpenApi.Config.File "./example/uuid-array-param.yaml") + viaggiatreno : OpenApi.Config.Input viaggiatreno = OpenApi.Config.inputFrom (OpenApi.Config.File "./example/viaggiatreno.yaml") - bug : Int -> OpenApi.Config.Input - bug n = - OpenApi.Config.inputFrom (OpenApi.Config.File ("./example/openapi-generator-bugs/" ++ String.fromInt n ++ ".yaml")) - - dbFahrplanApi : OpenApi.Config.Input - dbFahrplanApi = - OpenApi.Config.inputFrom (OpenApi.Config.File "./example/db-fahrplan-api-specification.yaml") - - gitHub : OpenApi.Config.Input - gitHub = - OpenApi.Config.inputFrom (OpenApi.Config.File "./example/github-spec.json") - |> OpenApi.Config.withWarnOnMissingEnums False - - patreon : OpenApi.Config.Input - patreon = - OpenApi.Config.inputFrom (OpenApi.Config.File "./example/patreon.json") - - telegramBot : OpenApi.Config.Input - telegramBot = - OpenApi.Config.inputFrom (OpenApi.Config.File "./example/telegram-bot.json") - profileConfig : OpenApi.Config.Config profileConfig = -- Slimmed config for profiling @@ -133,6 +137,7 @@ run = |> OpenApi.Config.withInput marioPartyStats |> OpenApi.Config.withInput nullableEnum |> OpenApi.Config.withInput overridingGlobalSecurity + |> OpenApi.Config.withInput pathLevelParams |> OpenApi.Config.withInput realworldConduit |> OpenApi.Config.withInput recursiveAllOfRefs |> OpenApi.Config.withInput simpleRef diff --git a/review/suppressed/NoUnused.Exports.json b/review/suppressed/NoUnused.Exports.json index 179de1b4..46dd9b74 100644 --- a/review/suppressed/NoUnused.Exports.json +++ b/review/suppressed/NoUnused.Exports.json @@ -3,6 +3,7 @@ "automatically created by": "elm-review suppress", "learn more": "elm-review suppress --help", "suppressions": [ + { "count": 1, "filePath": "src/CliMonad.elm" }, { "count": 1, "filePath": "src/OpenApi/Common/Internal.elm" } ] } diff --git a/src/OpenApi/Generate.elm b/src/OpenApi/Generate.elm index b7853ff7..55f1fce9 100644 --- a/src/OpenApi/Generate.elm +++ b/src/OpenApi/Generate.elm @@ -273,6 +273,46 @@ stripTrailingSlash input = input +{-| Merge path-level parameters with operation-level parameters. +Per the OpenAPI spec, operation-level parameters override path-level +parameters with the same name and location. +-} +mergeParams : + List (OpenApi.Reference.ReferenceOr OpenApi.Parameter.Parameter) + -> List (OpenApi.Reference.ReferenceOr OpenApi.Parameter.Parameter) + -> CliMonad (List (OpenApi.Reference.ReferenceOr OpenApi.Parameter.Parameter)) +mergeParams pathParams operationParams = + let + paramKey : OpenApi.Reference.ReferenceOr OpenApi.Parameter.Parameter -> CliMonad String + paramKey param = + toConcreteParam param + |> CliMonad.map (\concrete -> OpenApi.Parameter.in_ concrete ++ ":" ++ OpenApi.Parameter.name concrete) + in + CliMonad.combineMap paramKey operationParams + |> CliMonad.andThen + (\operationParamKeysList -> + let + operationParamKeys : FastSet.Set String + operationParamKeys = + FastSet.fromList operationParamKeysList + in + pathParams + |> CliMonad.foldl + (\param acc -> + paramKey param + |> CliMonad.map + (\key -> + if FastSet.member key operationParamKeys then + acc + + else + param :: acc + ) + ) + (CliMonad.succeed operationParams) + ) + + pathDeclarations : List OpenApi.Config.EffectType -> ServerInfo -> CliMonad (List CliMonad.Declaration) pathDeclarations effectTypes server = CliMonad.getApiSpec @@ -294,7 +334,7 @@ pathDeclarations effectTypes server = |> List.filterMap (\( method, getter ) -> Maybe.map (Tuple.pair method) (getter path)) |> CliMonad.combineMap (\( method, operation ) -> - toRequestFunctions server effectTypes method url operation + toRequestFunctions server effectTypes method url (OpenApi.Path.parameters path) operation |> CliMonad.errorToWarning ) |> CliMonad.map (List.filterMap identity >> List.concat) @@ -462,8 +502,8 @@ requestBodyToDeclarations name reference = |> CliMonad.withPath name -toRequestFunctions : ServerInfo -> List OpenApi.Config.EffectType -> String -> String -> OpenApi.Operation.Operation -> CliMonad (List CliMonad.Declaration) -toRequestFunctions server effectTypes method pathUrl operation = +toRequestFunctions : ServerInfo -> List OpenApi.Config.EffectType -> String -> String -> List (OpenApi.Reference.ReferenceOr OpenApi.Parameter.Parameter) -> OpenApi.Operation.Operation -> CliMonad (List CliMonad.Declaration) +toRequestFunctions server effectTypes method pathUrl pathLevelParams operation = let functionName : String functionName = @@ -1111,8 +1151,8 @@ toRequestFunctions server effectTypes method pathUrl operation = ) ] in - CliMonad.andThen3 - (\contentSchema auth successAnnotation -> + CliMonad.andThen4 + (\contentSchema auth successAnnotation allParams -> CliMonad.andThen4 (\toBody configAnnotation replaced toHeaderParams -> CliMonad.map2 (++) @@ -1142,7 +1182,7 @@ toRequestFunctions server effectTypes method pathUrl operation = |> CliMonad.andThen (\params -> toConfigParamAnnotation - { operation = operation + { allParams = allParams , successAnnotation = successAnnotation , errorBodyAnnotation = bodyTypeAnnotation , errorTypeAnnotation = errorTypeAnnotation @@ -1152,8 +1192,8 @@ toRequestFunctions server effectTypes method pathUrl operation = } ) ) - (replacedUrl server auth pathUrl operation) - (operationToHeaderParams operation) + (replacedUrl server auth pathUrl allParams) + (operationToHeaderParams allParams) ) (operationToContentSchema operation) (operationToAuthorizationInfo operation) @@ -1164,6 +1204,7 @@ toRequestFunctions server effectTypes method pathUrl operation = SuccessReference ref -> CliMonad.refToAnnotation ref ) + (mergeParams pathLevelParams (OpenApi.Operation.parameters operation)) in operationToTypesExpectAndResolver functionName operation |> CliMonad.andThen step @@ -1181,10 +1222,9 @@ operationToGroup operation = "Operations" -operationToHeaderParams : OpenApi.Operation.Operation -> CliMonad (List (Elm.Expression -> ( Elm.Expression, Elm.Expression, Bool ))) -operationToHeaderParams operation = - operation - |> OpenApi.Operation.parameters +operationToHeaderParams : List (OpenApi.Reference.ReferenceOr OpenApi.Parameter.Parameter) -> CliMonad (List (Elm.Expression -> ( Elm.Expression, Elm.Expression, Bool ))) +operationToHeaderParams params = + params |> CliMonad.combineMap (\param -> toConcreteParam param @@ -1230,8 +1270,8 @@ operationToHeaderParams operation = |> CliMonad.map (List.filterMap identity) -replacedUrl : ServerInfo -> AuthorizationInfo -> String -> OpenApi.Operation.Operation -> CliMonad (Elm.Expression -> Elm.Expression) -replacedUrl server authInfo pathUrl operation = +replacedUrl : ServerInfo -> AuthorizationInfo -> String -> List (OpenApi.Reference.ReferenceOr OpenApi.Parameter.Parameter) -> CliMonad (Elm.Expression -> Elm.Expression) +replacedUrl server authInfo pathUrl params = let pathSegments : List String pathSegments = @@ -1309,8 +1349,7 @@ replacedUrl server authInfo pathUrl operation = MultipleServers _ -> Gen.Url.Builder.call_.crossOrigin (Elm.get "server" config) (Elm.list replacedSegments) allQueryParams in - operation - |> OpenApi.Operation.parameters + params |> CliMonad.combineMap (\param -> toConcreteParam param @@ -1761,7 +1800,7 @@ contentToContentSchema content = toConfigParamAnnotation : - { operation : OpenApi.Operation.Operation + { allParams : List (OpenApi.Reference.ReferenceOr OpenApi.Parameter.Parameter) , successAnnotation : Elm.Annotation.Annotation , errorBodyAnnotation : Elm.Annotation.Annotation , errorTypeAnnotation : Elm.Annotation.Annotation @@ -1820,7 +1859,7 @@ toConfigParamAnnotation options = , lamderaProgramTest = toAnnotation toMsgLamderaProgramTest } ) - (operationToUrlParams options.operation) + (operationToUrlParams options.allParams) type ServerInfo @@ -1886,13 +1925,8 @@ serverInfo server = |> CliMonad.succeed -operationToUrlParams : OpenApi.Operation.Operation -> CliMonad (List ( Common.UnsafeName, Elm.Annotation.Annotation )) -operationToUrlParams operation = - let - params : List (OpenApi.Reference.ReferenceOr OpenApi.Parameter.Parameter) - params = - OpenApi.Operation.parameters operation - in +operationToUrlParams : List (OpenApi.Reference.ReferenceOr OpenApi.Parameter.Parameter) -> CliMonad (List ( Common.UnsafeName, Elm.Annotation.Annotation )) +operationToUrlParams params = if List.isEmpty params then CliMonad.succeed [] diff --git a/tests/Test/OpenApi/Generate.elm b/tests/Test/OpenApi/Generate.elm index 4133219d..667385f7 100644 --- a/tests/Test/OpenApi/Generate.elm +++ b/tests/Test/OpenApi/Generate.elm @@ -1,4 +1,4 @@ -module Test.OpenApi.Generate exposing (fuzzInputName, fuzzTitle, issue48, pr267, uuidArrayParam) +module Test.OpenApi.Generate exposing (fuzzInputName, fuzzTitle, issue48, pathLevelParams, pr267, uuidArrayParam) import Ansi.Color import CliMonad @@ -587,6 +587,174 @@ uuidArrayParam = ) +pathLevelParams : Test +pathLevelParams = + Test.test "Path-level parameters are merged into operations" <| + \() -> + let + oasString : String + oasString = + String.Multiline.here """ + openapi: "3.1.0" + info: + title: "Path Level Params Test" + version: "1.0.0" + paths: + /orgs/{orgId}/teams/{teamId}/items: + parameters: + - $ref: '#/components/parameters/orgIdParam' + - in: path + name: teamId + required: true + schema: + type: string + get: + operationId: getItems + parameters: + - in: query + name: status + required: false + schema: + type: string + responses: + "200": + description: OK + content: + application/json: + schema: + type: object + properties: + count: + type: integer + required: + - count + components: + parameters: + orgIdParam: + in: path + name: orgId + required: true + schema: + type: string + """ + 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 ] + , server = OpenApi.Config.Default + , formats = OpenApi.Config.defaultFormats + , warnOnMissingEnums = True + , keepGoing = False + } + oas + in + case genFiles of + Err e -> + Expect.fail ("Error in generation: " ++ Debug.toString e) + + Ok { modules } -> + case modules of + [ apiFile ] -> + let + apiFileString : String + apiFileString = + String.Multiline.here """ + module Output.Api exposing ( getItems ) + + {-| + @docs getItems + -} + + + import Dict + import Http + import Json.Decode + import OpenApi.Common + import Url.Builder + + + {- ## Operations -} + + + getItems : + { toMsg : Result (OpenApi.Common.Error e String) { count : Int } -> msg + , params : { orgId : String, teamId : String, status : Maybe String } + } + -> Cmd msg + getItems config = + Http.request + { url = + Url.Builder.absolute + [ "orgs" + , config.params.orgId + , "teams" + , config.params.teamId + , "items" + ] + (List.filterMap + Basics.identity + [ Maybe.map + (Url.Builder.string "status") + config.params.status + ] + ) + , method = "GET" + , headers = [] + , expect = + OpenApi.Common.expectJsonCustom + (Dict.fromList []) + (Json.Decode.succeed + (\\count -> { count = count } + ) |> OpenApi.Common.jsonDecodeAndMap + (Json.Decode.field "count" Json.Decode.int) + ) + config.toMsg + , body = Http.emptyBody + , timeout = Nothing + , tracker = Nothing + } + """ + in + expectEqualMultiline apiFileString (fileToString apiFile) + + _ -> + Expect.fail + ("Expected to generate 1 file but found " + ++ (List.length modules |> String.fromInt) + ++ ": " + ++ moduleNames modules + ) + + yamlToJsonValueDecoder : Yaml.Decode.Decoder Json.Encode.Value yamlToJsonValueDecoder = Yaml.Decode.oneOf