Skip to content

Commit 5d7c6bc

Browse files
committed
Support path-level parameters in code generation
Merge path item parameters with operation parameters per the OpenAPI spec. Previously, parameters defined at the path level (shared across all operations) were silently ignored, causing path params like {orgId} to appear as literal strings in generated URLs.
1 parent feb88e5 commit 5d7c6bc

4 files changed

Lines changed: 251 additions & 24 deletions

File tree

cli/example/path-level-params.yaml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
openapi: "3.1.0"
2+
info:
3+
title: "Path Level Params Test"
4+
version: "1.0.0"
5+
paths:
6+
/orgs/{orgId}/items:
7+
parameters:
8+
- in: path
9+
name: orgId
10+
required: true
11+
schema:
12+
type: string
13+
get:
14+
operationId: getItems
15+
parameters:
16+
- in: query
17+
name: status
18+
required: false
19+
schema:
20+
type: string
21+
responses:
22+
"200":
23+
description: OK
24+
content:
25+
application/json:
26+
schema:
27+
type: object
28+
properties:
29+
count:
30+
type: integer
31+
required:
32+
- count

cli/src/TestGenScript.elm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ run =
7676
singleEnum =
7777
OpenApi.Config.inputFrom (OpenApi.Config.File "./example/single-enum.yaml")
7878

79+
pathLevelParams : OpenApi.Config.Input
80+
pathLevelParams =
81+
OpenApi.Config.inputFrom (OpenApi.Config.File "./example/path-level-params.yaml")
82+
7983
trustmark : OpenApi.Config.Input
8084
trustmark =
8185
OpenApi.Config.inputFrom (OpenApi.Config.File "./example/trustmark.json")
@@ -133,6 +137,7 @@ run =
133137
|> OpenApi.Config.withInput recursiveAllOfRefs
134138
|> OpenApi.Config.withInput simpleRef
135139
|> OpenApi.Config.withInput singleEnum
140+
|> OpenApi.Config.withInput pathLevelParams
136141
|> OpenApi.Config.withInput trustmark
137142
|> OpenApi.Config.withInput trustmarkTradeCheck
138143
|> OpenApi.Config.withInput viaggiatreno

src/OpenApi/Generate.elm

Lines changed: 59 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,46 @@ stripTrailingSlash input =
273273
input
274274

275275

276+
{-| Merge path-level parameters with operation-level parameters.
277+
Per the OpenAPI spec, operation-level parameters override path-level
278+
parameters with the same name and location.
279+
-}
280+
mergeParams :
281+
List (OpenApi.Reference.ReferenceOr OpenApi.Parameter.Parameter)
282+
-> List (OpenApi.Reference.ReferenceOr OpenApi.Parameter.Parameter)
283+
-> List (OpenApi.Reference.ReferenceOr OpenApi.Parameter.Parameter)
284+
mergeParams pathParams operationParams =
285+
let
286+
operationParamKeys : FastSet.Set String
287+
operationParamKeys =
288+
operationParams
289+
|> List.filterMap
290+
(\param ->
291+
case OpenApi.Reference.toConcrete param of
292+
Just concrete ->
293+
Just (OpenApi.Parameter.in_ concrete ++ ":" ++ OpenApi.Parameter.name concrete)
294+
295+
Nothing ->
296+
Nothing
297+
)
298+
|> FastSet.fromList
299+
300+
nonOverriddenPathParams : List (OpenApi.Reference.ReferenceOr OpenApi.Parameter.Parameter)
301+
nonOverriddenPathParams =
302+
pathParams
303+
|> List.filter
304+
(\param ->
305+
case OpenApi.Reference.toConcrete param of
306+
Just concrete ->
307+
not (FastSet.member (OpenApi.Parameter.in_ concrete ++ ":" ++ OpenApi.Parameter.name concrete) operationParamKeys)
308+
309+
Nothing ->
310+
True
311+
)
312+
in
313+
nonOverriddenPathParams ++ operationParams
314+
315+
276316
pathDeclarations : List OpenApi.Config.EffectType -> ServerInfo -> CliMonad (List CliMonad.Declaration)
277317
pathDeclarations effectTypes server =
278318
CliMonad.getApiSpec
@@ -294,7 +334,7 @@ pathDeclarations effectTypes server =
294334
|> List.filterMap (\( method, getter ) -> Maybe.map (Tuple.pair method) (getter path))
295335
|> CliMonad.combineMap
296336
(\( method, operation ) ->
297-
toRequestFunctions server effectTypes method url operation
337+
toRequestFunctions server effectTypes method url (OpenApi.Path.parameters path) operation
298338
|> CliMonad.errorToWarning
299339
)
300340
|> CliMonad.map (List.filterMap identity >> List.concat)
@@ -462,9 +502,12 @@ requestBodyToDeclarations name reference =
462502
|> CliMonad.withPath name
463503

464504

465-
toRequestFunctions : ServerInfo -> List OpenApi.Config.EffectType -> String -> String -> OpenApi.Operation.Operation -> CliMonad (List CliMonad.Declaration)
466-
toRequestFunctions server effectTypes method pathUrl operation =
505+
toRequestFunctions : ServerInfo -> List OpenApi.Config.EffectType -> String -> String -> List (OpenApi.Reference.ReferenceOr OpenApi.Parameter.Parameter) -> OpenApi.Operation.Operation -> CliMonad (List CliMonad.Declaration)
506+
toRequestFunctions server effectTypes method pathUrl pathLevelParams operation =
467507
let
508+
allParams : List (OpenApi.Reference.ReferenceOr OpenApi.Parameter.Parameter)
509+
allParams =
510+
mergeParams pathLevelParams (OpenApi.Operation.parameters operation)
468511
functionName : String
469512
functionName =
470513
OpenApi.Operation.operationId operation
@@ -1142,7 +1185,7 @@ toRequestFunctions server effectTypes method pathUrl operation =
11421185
|> CliMonad.andThen
11431186
(\params ->
11441187
toConfigParamAnnotation
1145-
{ operation = operation
1188+
{ allParams = allParams
11461189
, successAnnotation = successAnnotation
11471190
, errorBodyAnnotation = bodyTypeAnnotation
11481191
, errorTypeAnnotation = errorTypeAnnotation
@@ -1152,8 +1195,8 @@ toRequestFunctions server effectTypes method pathUrl operation =
11521195
}
11531196
)
11541197
)
1155-
(replacedUrl server auth pathUrl operation)
1156-
(operationToHeaderParams operation)
1198+
(replacedUrl server auth pathUrl allParams)
1199+
(operationToHeaderParams allParams)
11571200
)
11581201
(operationToContentSchema operation)
11591202
(operationToAuthorizationInfo operation)
@@ -1181,10 +1224,9 @@ operationToGroup operation =
11811224
"Operations"
11821225

11831226

1184-
operationToHeaderParams : OpenApi.Operation.Operation -> CliMonad (List (Elm.Expression -> ( Elm.Expression, Elm.Expression, Bool )))
1185-
operationToHeaderParams operation =
1186-
operation
1187-
|> OpenApi.Operation.parameters
1227+
operationToHeaderParams : List (OpenApi.Reference.ReferenceOr OpenApi.Parameter.Parameter) -> CliMonad (List (Elm.Expression -> ( Elm.Expression, Elm.Expression, Bool )))
1228+
operationToHeaderParams params =
1229+
params
11881230
|> CliMonad.combineMap
11891231
(\param ->
11901232
toConcreteParam param
@@ -1230,8 +1272,8 @@ operationToHeaderParams operation =
12301272
|> CliMonad.map (List.filterMap identity)
12311273

12321274

1233-
replacedUrl : ServerInfo -> AuthorizationInfo -> String -> OpenApi.Operation.Operation -> CliMonad (Elm.Expression -> Elm.Expression)
1234-
replacedUrl server authInfo pathUrl operation =
1275+
replacedUrl : ServerInfo -> AuthorizationInfo -> String -> List (OpenApi.Reference.ReferenceOr OpenApi.Parameter.Parameter) -> CliMonad (Elm.Expression -> Elm.Expression)
1276+
replacedUrl server authInfo pathUrl params =
12351277
let
12361278
pathSegments : List String
12371279
pathSegments =
@@ -1309,8 +1351,7 @@ replacedUrl server authInfo pathUrl operation =
13091351
MultipleServers _ ->
13101352
Gen.Url.Builder.call_.crossOrigin (Elm.get "server" config) (Elm.list replacedSegments) allQueryParams
13111353
in
1312-
operation
1313-
|> OpenApi.Operation.parameters
1354+
params
13141355
|> CliMonad.combineMap
13151356
(\param ->
13161357
toConcreteParam param
@@ -1761,7 +1802,7 @@ contentToContentSchema content =
17611802

17621803

17631804
toConfigParamAnnotation :
1764-
{ operation : OpenApi.Operation.Operation
1805+
{ allParams : List (OpenApi.Reference.ReferenceOr OpenApi.Parameter.Parameter)
17651806
, successAnnotation : Elm.Annotation.Annotation
17661807
, errorBodyAnnotation : Elm.Annotation.Annotation
17671808
, errorTypeAnnotation : Elm.Annotation.Annotation
@@ -1820,7 +1861,7 @@ toConfigParamAnnotation options =
18201861
, lamderaProgramTest = toAnnotation toMsgLamderaProgramTest
18211862
}
18221863
)
1823-
(operationToUrlParams options.operation)
1864+
(operationToUrlParams options.allParams)
18241865

18251866

18261867
type ServerInfo
@@ -1886,13 +1927,8 @@ serverInfo server =
18861927
|> CliMonad.succeed
18871928

18881929

1889-
operationToUrlParams : OpenApi.Operation.Operation -> CliMonad (List ( Common.UnsafeName, Elm.Annotation.Annotation ))
1890-
operationToUrlParams operation =
1891-
let
1892-
params : List (OpenApi.Reference.ReferenceOr OpenApi.Parameter.Parameter)
1893-
params =
1894-
OpenApi.Operation.parameters operation
1895-
in
1930+
operationToUrlParams : List (OpenApi.Reference.ReferenceOr OpenApi.Parameter.Parameter) -> CliMonad (List ( Common.UnsafeName, Elm.Annotation.Annotation ))
1931+
operationToUrlParams params =
18961932
if List.isEmpty params then
18971933
CliMonad.succeed []
18981934

tests/Test/OpenApi/Generate.elm

Lines changed: 155 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
module Test.OpenApi.Generate exposing (fuzzInputName, fuzzTitle, issue48, pr267)
1+
module Test.OpenApi.Generate exposing (fuzzInputName, fuzzTitle, issue48, pathLevelParams, pr267)
22

33
import Ansi.Color
44
import CliMonad
@@ -478,6 +478,160 @@ pr267 =
478478
)
479479

480480

481+
pathLevelParams : Test
482+
pathLevelParams =
483+
Test.test "Path-level parameters are merged into operations" <|
484+
\() ->
485+
let
486+
oasString : String
487+
oasString =
488+
String.Multiline.here """
489+
openapi: "3.1.0"
490+
info:
491+
title: "Path Level Params Test"
492+
version: "1.0.0"
493+
paths:
494+
/orgs/{orgId}/items:
495+
parameters:
496+
- in: path
497+
name: orgId
498+
required: true
499+
schema:
500+
type: string
501+
get:
502+
operationId: getItems
503+
parameters:
504+
- in: query
505+
name: status
506+
required: false
507+
schema:
508+
type: string
509+
responses:
510+
"200":
511+
description: OK
512+
content:
513+
application/json:
514+
schema:
515+
type: object
516+
properties:
517+
count:
518+
type: integer
519+
required:
520+
- count
521+
"""
522+
in
523+
case
524+
oasString
525+
|> Yaml.Decode.fromString yamlToJsonValueDecoder
526+
|> Result.mapError Debug.toString
527+
|> Result.andThen
528+
(\json ->
529+
json
530+
|> Json.Decode.decodeValue OpenApi.decode
531+
|> Result.mapError Debug.toString
532+
)
533+
of
534+
Err e ->
535+
Expect.fail e
536+
537+
Ok oas ->
538+
let
539+
genFiles :
540+
Result
541+
CliMonad.Message
542+
{ modules :
543+
List
544+
{ moduleName : List String
545+
, declarations : FastDict.Dict String { group : String, declaration : Elm.Declaration }
546+
}
547+
, warnings : List CliMonad.Message
548+
, requiredPackages : FastSet.Set String
549+
}
550+
genFiles =
551+
OpenApi.Generate.files
552+
{ namespace = [ "Output" ]
553+
, generateTodos = False
554+
, effectTypes = [ OpenApi.Config.ElmHttpCmd ]
555+
, server = OpenApi.Config.Default
556+
, formats = OpenApi.Config.defaultFormats
557+
, warnOnMissingEnums = True
558+
, keepGoing = False
559+
}
560+
oas
561+
in
562+
case genFiles of
563+
Err e ->
564+
Expect.fail ("Error in generation: " ++ Debug.toString e)
565+
566+
Ok { modules } ->
567+
case modules of
568+
[ apiFile ] ->
569+
let
570+
apiFileString : String
571+
apiFileString =
572+
String.Multiline.here """
573+
module Output.Api exposing ( getItems )
574+
575+
{-|
576+
@docs getItems
577+
-}
578+
579+
580+
import Dict
581+
import Http
582+
import Json.Decode
583+
import OpenApi.Common
584+
import Url.Builder
585+
586+
587+
{- ## Operations -}
588+
589+
590+
getItems :
591+
{ toMsg : Result (OpenApi.Common.Error e String) { count : Int } -> msg
592+
, params : { orgId : String, status : Maybe String }
593+
}
594+
-> Cmd msg
595+
getItems config =
596+
Http.request
597+
{ url =
598+
Url.Builder.absolute
599+
[ "orgs", config.params.orgId, "items" ]
600+
(List.filterMap
601+
Basics.identity
602+
[ Maybe.map
603+
(Url.Builder.string "status")
604+
config.params.status
605+
]
606+
)
607+
, method = "GET"
608+
, headers = []
609+
, expect =
610+
OpenApi.Common.expectJsonCustom
611+
(Dict.fromList [])
612+
(Json.Decode.succeed
613+
(\\count -> { count = count }
614+
) |> OpenApi.Common.jsonDecodeAndMap
615+
(Json.Decode.field "count" Json.Decode.int)
616+
)
617+
config.toMsg
618+
, body = Http.emptyBody
619+
, timeout = Nothing
620+
, tracker = Nothing
621+
}
622+
"""
623+
in
624+
expectEqualMultiline apiFileString (fileToString apiFile)
625+
626+
_ ->
627+
Expect.fail
628+
("Expected to generate 1 file but found "
629+
++ (List.length modules |> String.fromInt)
630+
++ ": "
631+
++ moduleNames modules
632+
)
633+
634+
481635
yamlToJsonValueDecoder : Yaml.Decode.Decoder Json.Encode.Value
482636
yamlToJsonValueDecoder =
483637
Yaml.Decode.oneOf

0 commit comments

Comments
 (0)