Skip to content

Commit bc3556e

Browse files
committed
Handle recursive objects by wrapping them in a newtype
1 parent 3af46b5 commit bc3556e

13 files changed

Lines changed: 383 additions & 65 deletions

File tree

cli/example/recursive.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
openapi: 3.0.1
2+
info:
3+
title: "Recursive"
4+
version: "1"
5+
components:
6+
schemas:
7+
Recursive:
8+
description: Recursive type
9+
type: object
10+
properties:
11+
date:
12+
type: string
13+
format: date
14+
parent:
15+
description: Tag information
16+
$ref: "#/components/schemas/Recursive"

cli/example/src/Example.elm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import PatreonApi.Api
1818
import PatreonApi.Types.Responses
1919
import RealworldConduitApi.Api
2020
import RealworldConduitApi.Types.Responses
21+
import Recursive.Json
22+
import Recursive.Types
2123
import RecursiveAllofRefs.Types
2224
import SimpleRef.Json
2325
import SingleEnum.Types
@@ -49,6 +51,9 @@ init () =
4951

5052
_ =
5153
MultipartFormData.Api.api
54+
55+
_ =
56+
Recursive.Json.encodeRecursive
5257
in
5358
( {}
5459
, Cmd.batch

cli/src/TestGenScript.elm

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ run =
8989
realworldConduit =
9090
OpenApi.Config.inputFrom (OpenApi.Config.File "./example/realworld-conduit.yaml")
9191

92+
recursive : OpenApi.Config.Input
93+
recursive =
94+
OpenApi.Config.inputFrom (OpenApi.Config.File "./example/recursive.yaml")
95+
9296
recursiveAllOfRefs : OpenApi.Config.Input
9397
recursiveAllOfRefs =
9498
OpenApi.Config.inputFrom (OpenApi.Config.File "./example/recursive-allof-refs.yaml")
@@ -149,6 +153,7 @@ run =
149153
|> OpenApi.Config.withInput overridingGlobalSecurity
150154
|> OpenApi.Config.withInput pathLevelParams
151155
|> OpenApi.Config.withInput realworldConduit
156+
|> OpenApi.Config.withInput recursive
152157
|> OpenApi.Config.withInput recursiveAllOfRefs
153158
|> OpenApi.Config.withInput simpleRef
154159
|> OpenApi.Config.withInput singleEnum

review/suppressed/NoUnused.Exports.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"automatically created by": "elm-review suppress",
44
"learn more": "elm-review suppress --help",
55
"suppressions": [
6-
{ "count": 1, "filePath": "src/CliMonad.elm" },
6+
{ "count": 2, "filePath": "src/CliMonad.elm" },
77
{ "count": 1, "filePath": "src/OpenApi/Common/Internal.elm" }
88
]
99
}

src/CliMonad.elm

Lines changed: 83 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ module CliMonad exposing
33
, run, stepOrFail
44
, succeed, succeedWith, fail, fromResult
55
, map, map2, map3
6-
, andThen, andThen2, andThen3, andThen4, combine, combineDict, combineMap, foldl
7-
, errorToWarning, getApiSpec, enumName, moduleToNamespace, getOrCache
6+
, andThen, andThen2, andThen3, andThen4, combine, combineDict, combineMap, foldl, any, findMap
7+
, errorToWarning, getApiSpec, enumName, moduleToNamespace, getOrCacheIsRecursive, getOrCacheType
88
, withPath, withWarning, withExtendedWarning, withRequiredPackage
99
, todo, todoWithDefault
1010
, withFormat
@@ -18,8 +18,8 @@ module CliMonad exposing
1818
@docs run, stepOrFail
1919
@docs succeed, succeedWith, fail, fromResult
2020
@docs map, map2, map3
21-
@docs andThen, andThen2, andThen3, andThen4, combine, combineDict, combineMap, foldl
22-
@docs errorToWarning, getApiSpec, enumName, moduleToNamespace, getOrCache
21+
@docs andThen, andThen2, andThen3, andThen4, combine, combineDict, combineMap, foldl, any, findMap
22+
@docs errorToWarning, getApiSpec, enumName, moduleToNamespace, getOrCacheIsRecursive, getOrCacheType
2323
@docs withPath, withWarning, withExtendedWarning, withRequiredPackage
2424
@docs todo, todoWithDefault
2525
@docs withFormat
@@ -99,7 +99,9 @@ type CliMonad a
9999

100100

101101
type alias Cache =
102-
FastDict.Dict String Common.Type
102+
{ typeCache : FastDict.Dict String Common.Type
103+
, isRecursiveCache : FastDict.Dict String (Maybe Common.UnsafeName)
104+
}
103105

104106

105107
type alias Output =
@@ -200,26 +202,54 @@ run oneOfDeclarations input (CliMonad x) =
200202

201203
emptyCache : Cache
202204
emptyCache =
203-
FastDict.empty
205+
{ typeCache = FastDict.empty
206+
, isRecursiveCache = FastDict.empty
207+
}
208+
209+
210+
getOrCacheType : Common.RefTo Common.Schema -> (() -> CliMonad Common.Type) -> CliMonad Common.Type
211+
getOrCacheType ref compute =
212+
let
213+
(Common.UnsafeName key) =
214+
Common.refToString ref
215+
in
216+
CliMonad
217+
(\input cache ->
218+
case FastDict.get key cache.typeCache of
219+
Nothing ->
220+
let
221+
(CliMonad inner) =
222+
compute ()
223+
in
224+
case inner input cache of
225+
Ok ( computed, output, cache2 ) ->
226+
Ok ( computed, output, { cache2 | typeCache = FastDict.insert key computed cache2.typeCache } )
227+
228+
Err e ->
229+
Err e
230+
231+
Just found ->
232+
Ok ( found, emptyOutput, cache )
233+
)
204234

205235

206-
getOrCache : Common.RefTo Common.Schema -> (() -> CliMonad Common.Type) -> CliMonad Common.Type
207-
getOrCache ref compute =
236+
getOrCacheIsRecursive : Common.RefTo Common.Schema -> (() -> CliMonad (Maybe Common.UnsafeName)) -> CliMonad (Maybe Common.UnsafeName)
237+
getOrCacheIsRecursive ref compute =
208238
let
209239
(Common.UnsafeName key) =
210240
Common.refToString ref
211241
in
212242
CliMonad
213243
(\input cache ->
214-
case FastDict.get key cache of
244+
case FastDict.get key cache.isRecursiveCache of
215245
Nothing ->
216246
let
217247
(CliMonad inner) =
218248
compute ()
219249
in
220250
case inner input cache of
221251
Ok ( computed, output, cache2 ) ->
222-
Ok ( computed, output, FastDict.insert key computed cache2 )
252+
Ok ( computed, output, { cache2 | isRecursiveCache = FastDict.insert key computed cache2.isRecursiveCache } )
223253

224254
Err e ->
225255
Err e
@@ -443,6 +473,49 @@ map4 f (CliMonad x) (CliMonad y) (CliMonad z) (CliMonad w) =
443473
)
444474

445475

476+
any : (a -> CliMonad Bool) -> List a -> CliMonad Bool
477+
any f xs =
478+
CliMonad
479+
(\input cache ->
480+
Result.Extra.foldlWhileOk
481+
(\x ( a, o, c ) ->
482+
if a then
483+
Ok ( a, o, c )
484+
485+
else
486+
let
487+
(CliMonad i) =
488+
f x
489+
in
490+
i input c
491+
)
492+
( False, emptyOutput, cache )
493+
xs
494+
)
495+
496+
497+
findMap : (a -> CliMonad (Maybe b)) -> List a -> CliMonad (Maybe b)
498+
findMap f xs =
499+
CliMonad
500+
(\input cache ->
501+
Result.Extra.foldlWhileOk
502+
(\x ( a, o, c ) ->
503+
case a of
504+
Just _ ->
505+
Ok ( a, o, c )
506+
507+
Nothing ->
508+
let
509+
(CliMonad i) =
510+
f x
511+
in
512+
i input c
513+
)
514+
( Nothing, emptyOutput, cache )
515+
xs
516+
)
517+
518+
446519
andThen : (a -> CliMonad b) -> CliMonad a -> CliMonad b
447520
andThen f (CliMonad x) =
448521
CliMonad

src/Common.elm

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,7 @@ initialUppercaseWordToLowercase input =
446446

447447
type Type
448448
= Nullable Type
449-
| Object Object
449+
| Object { isRecursive : Maybe UnsafeName } Object
450450
| Basic
451451
-- This is separate for easier pattern matching
452452
BasicType

src/Elm/Extra.elm

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
module Elm.Extra exposing (functionReduced)
1+
module Elm.Extra exposing (functionReduced, withDocumentationMaybe)
22

33
import Elm
44
import Elm.ToString
@@ -12,3 +12,13 @@ functionReduced argName f =
1212

1313
else
1414
Elm.functionReduced argName f
15+
16+
17+
withDocumentationMaybe : Maybe String -> Elm.Declaration -> Elm.Declaration
18+
withDocumentationMaybe documentation declaration =
19+
case documentation of
20+
Nothing ->
21+
declaration
22+
23+
Just doc ->
24+
Elm.withDocumentation doc declaration

src/Gen/BackendTask.elm

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ module Gen.BackendTask exposing (annotation_)
55
66
# Generated bindings for BackendTask
77
8-
@docs map
9-
@docs map56
108
@docs annotation_
119
1210
-}

src/Gen/Json/Decode.elm

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module Gen.Json.Decode exposing
33
, list, dict, keyValuePairs, field
44
, index, oneOf
55
, map, map2, map3
6-
, value, null
6+
, lazy, value, null
77
, succeed, fail, andThen, annotation_
88
, call_, values_
99
)
@@ -17,7 +17,7 @@ module Gen.Json.Decode exposing
1717
@docs list, dict, keyValuePairs, field
1818
@docs index, oneOf
1919
@docs map, map2, map3
20-
@docs value, null
20+
@docs lazy, value, null
2121
@docs succeed, fail, andThen, annotation_
2222
@docs call_, values_
2323
@@ -596,6 +596,64 @@ map3 map3Arg_ map3Arg_0 map3Arg_1 map3Arg_2 =
596596
]
597597

598598

599+
{-| Sometimes you have JSON with recursive structure, like nested comments.
600+
You can use `lazy` to make sure your decoder unrolls lazily.
601+
602+
type alias Comment =
603+
{ message : String
604+
, responses : Responses
605+
}
606+
607+
type Responses
608+
= Responses (List Comment)
609+
610+
comment : Decoder Comment
611+
comment =
612+
map2 Comment
613+
(field "message" string)
614+
(field "responses" (map Responses (list (lazy (\_ -> comment)))))
615+
616+
If we had said `list comment` instead, we would start expanding the value
617+
infinitely. What is a `comment`? It is a decoder for objects where the
618+
`responses` field contains comments. What is a `comment` though? Etc.
619+
620+
By using `list (lazy (\_ -> comment))` we make sure the decoder only expands
621+
to be as deep as the JSON we are given. You can read more about recursive data
622+
structures [here].
623+
624+
[here]: https://github.com/elm/compiler/blob/master/hints/recursive-alias.md
625+
626+
lazy: (() -> Json.Decode.Decoder a) -> Json.Decode.Decoder a
627+
628+
-}
629+
lazy : (Elm.Expression -> Elm.Expression) -> Elm.Expression
630+
lazy lazyArg_ =
631+
Elm.apply
632+
(Elm.value
633+
{ importFrom = [ "Json", "Decode" ]
634+
, name = "lazy"
635+
, annotation =
636+
Just
637+
(Type.function
638+
[ Type.function
639+
[ Type.unit ]
640+
(Type.namedWith
641+
[ "Json", "Decode" ]
642+
"Decoder"
643+
[ Type.var "a" ]
644+
)
645+
]
646+
(Type.namedWith
647+
[ "Json", "Decode" ]
648+
"Decoder"
649+
[ Type.var "a" ]
650+
)
651+
)
652+
}
653+
)
654+
[ Elm.functionReduced "lazyUnpack" lazyArg_ ]
655+
656+
599657
{-| Do not do anything with a JSON value, just bring it into Elm as a `Value`.
600658
This can be useful if you have particularly complex data that you would like to
601659
deal with later. Or if you are going to send it out a port and do not care

0 commit comments

Comments
 (0)