Skip to content

Commit

Permalink
Merge pull request #18 from gbtb/dev
Browse files Browse the repository at this point in the history
Dev
  • Loading branch information
gbtb authored Aug 11, 2018
2 parents 23838cc + 7f4cd48 commit a04758b
Show file tree
Hide file tree
Showing 19 changed files with 623 additions and 7,554 deletions.
24 changes: 21 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,41 @@
![](https://travis-ci.org/gbtb/elm-gen.svg?branch=master)
![](https://travis-ci.org/gbtb/elm-gen.svg?branch=master) [![npm version](https://badge.fury.io/js/elm-gen.svg)](https://badge.fury.io/js/elm-gen)
# elm-gen
Experimental CLI tool for generating Elm JSON Decoders and Encoders, written mostly in Elm.
Based on top of [elm-ast](https://github.com/Bogdanp/elm-ast) parsing library.

## Usage

### Online demo
Simplified demonstration available [here](https://gbtb.github.io/elm-gen/).

### CLI tool
CLI tool can be obtained from [NPM](https://www.npmjs.com/package/elm-gen). After installation, you can used it like that:
`elm-gen decoders&encoders Model.elm .`
Also look at functional tests in ts/MainTests.ts and correspondent files in tests_data folder.
Call above will produce new file ModelEncodersAndDecoders.elm, which will contain decoders and encoders for types present in Model.elm.
Also note, that generated elm code for decoders uses NoRedInk elm-decode-pipeline package functions, so it must be installed into your project.
For more examples you may also look at functional tests in ts/MainTests.ts and correspondent files in tests_data folder.
## Features
* elm-gen can generate decoders and/or encoders for user defined Record and Union types without type variables.
* elm-gen supports basic elm types supported by Json.(Encode|Decode) plus tuples and Maybe's.
* elm-gen can use hand-made decoders and encoders `Decoder X` and `X -> Value` in order to generate decoders for types, dependent on type `X`
* elm-gen will follow **explicit** import type references to look for type definitions doesnot present in current file
* elm-gen dont require to break your Elm code in any way (its still compilable by `elm-make` without pre-processing), except for some [meta-comments](#meta-comments) insertion.

### Config
Some configuration (mostly about naming) can be applied from config json-file. Example of config can be found in tests_data folder.
It's correspondent Elm type representation resides in Config.elm

### Meta-Comments
Elm-gen supports meta-comments in source code, in form of either `-- //Meta-Comment` or `{-| //Meta-Comment -}`.
For now, there are 3 meta-comments:
For now, there are 4 meta-comments:

* Ignore -- elm-gen completely ignores next type definition (literally skipping it during parse stage).

* DefaultValue -- use next pair of (value type def, value expr) to produce default value for `Json.Decode.Pipeline.optional` function.
Supports values of types `defaultValue: UnionType` for union types, and `defaultValue : Record ` or `defaultValue : Record -> Record` for record types.

* NoDeclaration -- elm-gen won't generates type declaration for next type's decoder (usefull for preserving record structural typing ability for generated decoder )

* FieldNameMapping - TODO


12 changes: 10 additions & 2 deletions elm/src/Composer.elm
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ type alias GenContext =
, defaultRecordValues : Dict.Dict ( TypeName, String ) Expression
, defaultUnionValues : Dict.Dict TypeName Expression
, dontDeclareTypes : Set.Set TypeName
, fieldNameMapping : Dict.Dict String (Dict.Dict String String)
, fieldNameMappingApplications : Dict.Dict TypeName String
, generatorFunc : TransformationContext -> Statement -> Result String (List Statement)
, prefix : String
, makeName : String -> String
Expand Down Expand Up @@ -60,7 +62,7 @@ makeFileLoadRequest model =


{-| This function is designed to handle additional loading of type definitions came through fileLoadRequest
as well as initial loading of provided input file(s)
as well as initial loading of provided input file(s)
-}
resolveDependencies : Model -> Result String Model
resolveDependencies model =
Expand Down Expand Up @@ -158,7 +160,7 @@ resolveDependencies model =


{-| This func calculates unknown types from types used (userDefinedTypes) minus defined types in type-def dict,
minus those types that *could* be imported from wide imports (aka Import Dict)
minus those types that *could* be imported from wide imports (aka Import Dict)
-}
getUnknownTypes wideImports usedTypes definedTypes =
let
Expand Down Expand Up @@ -214,6 +216,8 @@ generate model =
, defaultRecordValues = model.defaultRecordValues
, defaultUnionValues = model.defaultUnionValues
, dontDeclareTypes = model.dontDeclareTypes
, fieldNameMapping = model.fieldNameMapping
, fieldNameMappingApplications = model.fieldNameMappingApplications
, generatorFunc = genDecoder
, prefix = getDecodePrefix model.config.jsonModulesImports.decode
, makeName = nameFunc
Expand All @@ -239,6 +243,8 @@ generate model =
, defaultRecordValues = model.defaultRecordValues
, defaultUnionValues = model.defaultUnionValues
, dontDeclareTypes = model.dontDeclareTypes
, fieldNameMapping = model.fieldNameMapping
, fieldNameMappingApplications = model.fieldNameMappingApplications
, generatorFunc = genEncoder
, prefix = getEncodePrefix model.config.jsonModulesImports.encode
, makeName = nameFunc
Expand Down Expand Up @@ -365,6 +371,8 @@ generateDecodersHelper genContext item =
genContext.defaultRecordValues
genContext.defaultUnionValues
genContext.dontDeclareTypes
genContext.fieldNameMapping
genContext.fieldNameMappingApplications
)
stmt

Expand Down
6 changes: 6 additions & 0 deletions elm/src/Model.elm
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ type MetaComment
= Ignore
| DefaultValue
| NoDeclaration
| FieldNameConversion
| FieldNameConversionApplication String


{-| //Ignore
Expand Down Expand Up @@ -59,6 +61,8 @@ type alias Model =
, providedEncoders : Dict.Dict TypeName String
, defaultRecordValues : Dict.Dict ( TypeName, String ) Expression
, defaultUnionValues : Dict.Dict TypeName Expression
, fieldNameMapping : Dict.Dict String (Dict.Dict String String)
, fieldNameMappingApplications : Dict.Dict TypeName String
, dontDeclareTypes : Set.Set TypeName
, generatedDecoders : List (List Statement)
, generatedEncoders : List (List Statement)
Expand All @@ -82,6 +86,8 @@ initModel =
, providedEncoders = Dict.empty
, defaultRecordValues = Dict.empty
, defaultUnionValues = Dict.empty
, fieldNameMapping = Dict.empty
, fieldNameMappingApplications = Dict.empty
, dontDeclareTypes = Set.empty
, generatedDecoders = []
, generatedEncoders = []
Expand Down
125 changes: 100 additions & 25 deletions elm/src/ParserExtensions.elm
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ applyMetaComments :
List Statement
-> { statements : List Statement
, defaultRecordValues : Dict.Dict ( TypeName, String ) Expression
, fieldNameMapping : Dict.Dict String (Dict.Dict String String)
, fieldNameMappingApplications : Dict.Dict TypeName String
, defaultUnionValues : Dict.Dict TypeName Expression
, dontDeclareTypes : Set.Set TypeName
}
Expand All @@ -23,6 +25,8 @@ applyMetaComments stmnts =
in
{ statements = List.reverse foldResult.statements
, defaultRecordValues = foldResult.defaultRecordValues
, fieldNameMapping = foldResult.fieldNameMapping
, fieldNameMappingApplications = foldResult.fieldNameMappingApplications
, defaultUnionValues = foldResult.defaultUnionValues
, dontDeclareTypes = foldResult.dontDeclareTypes
}
Expand All @@ -32,6 +36,8 @@ type alias FoldHelper =
{ metaComment : Maybe MetaComment
, typeName : Maybe TypeName
, defaultRecordValues : Dict.Dict ( TypeName, String ) Expression
, fieldNameMapping : Dict.Dict String (Dict.Dict String String)
, fieldNameMappingApplications : Dict.Dict TypeName String
, defaultUnionValues : Dict.Dict TypeName Expression
, dontDeclareTypes : Set.Set TypeName
, statements : List Statement
Expand All @@ -42,6 +48,8 @@ initFoldHelper =
{ metaComment = Nothing
, typeName = Nothing
, defaultRecordValues = Dict.empty
, fieldNameMapping = Dict.empty
, fieldNameMappingApplications = Dict.empty
, defaultUnionValues = Dict.empty
, dontDeclareTypes = Set.empty
, statements = []
Expand Down Expand Up @@ -70,37 +78,64 @@ foldHelper item accum =
Just meta ->
if (asFilter <| extractType item) && meta == Ignore then
f1
else if meta == NoDeclaration then
case extractType item of
Just ( typeName, _ ) ->
{ f2 | dontDeclareTypes = Set.insert typeName f2.dontDeclareTypes }

Nothing ->
f2
else if meta == DefaultValue then
case accum.typeName of
Nothing ->
case extractUnionTypeDefault item |> Maybe.orElse (extractRecordTypeDefault item) of
Just typeName ->
{ accum | typeName = Just typeName }

Nothing ->
f3

Just typeName ->
case extractDefaultValues accum item of
Just newAcc ->
newAcc

Nothing ->
f3
else
f3
metaCommentCaseHelper accum meta item f1 f2 f3

Nothing ->
f2


metaCommentCaseHelper accum meta item f1 f2 f3 =
case meta of
NoDeclaration ->
case extractType item of
Just ( typeName, _ ) ->
{ f2 | dontDeclareTypes = Set.insert typeName f2.dontDeclareTypes }

Nothing ->
f2

DefaultValue ->
case accum.typeName of
Nothing ->
case extractUnionTypeDefault item |> Maybe.orElse (extractRecordTypeDefault item) of
Just typeName ->
{ accum | typeName = Just typeName }

Nothing ->
f3

Just typeName ->
case extractDefaultValues accum item of
Just newAcc ->
newAcc

Nothing ->
f3

FieldNameConversion ->
case extractFieldNameConversion f1 item of
Just newAcc ->
newAcc

Nothing ->
f2

FieldNameConversionApplication conversionName ->
case extractType item of
Just ( typeName, _ ) ->
{ f2
| fieldNameMappingApplications =
Dict.insert typeName conversionName f2.fieldNameMappingApplications
}

Nothing ->
accum

_ ->
f3


defaultValueHelper accum item =
{ accum
| typeName =
Expand Down Expand Up @@ -158,3 +193,43 @@ extractRecordHelper accum typeName fieldList =
)
accum
fieldList


{-| This func tries to extract field name conversion
(aka special record mapping record field names to their aliases for decoding )
-}
extractFieldNameConversion accum item =
case item of
FunctionDeclaration funcName [] funcBody ->
extractRecord funcBody
|> Maybe.andThen
(\fields ->
List.foldl extractFieldNameConversionHelper (Just Dict.empty) fields
)
|> Maybe.map
(\dict ->
{ accum
| fieldNameMapping = Dict.insert funcName dict accum.fieldNameMapping
}
)

_ ->
Nothing


extractFieldNameConversionHelper ( name, expr ) accum =
case accum of
Nothing ->
Nothing

Just dict ->
case expr of
String s ->
Dict.insert name s dict |> Just

_ ->
Nothing


resetTypeName a =
{ a | typeName = Nothing }
32 changes: 24 additions & 8 deletions elm/src/StatementFilters.elm
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,30 @@ eeHelper tcName s =
extractMetaComment s =
case s of
Comment str ->
if Regex.contains (Regex.regex "//Ignore") str then
Just Ignore
else if Regex.contains (Regex.regex "//DefaultValue") str then
Just DefaultValue
else if Regex.contains (Regex.regex "//NoDeclaration") str then
Just NoDeclaration
else
Nothing
let
fieldNameApp =
Regex.find (Regex.AtMost 1) (Regex.regex ".*//UseFieldNameMapping\\(([\\d\\w]+)\\).*") str

submatch =
fieldNameApp
|> List.head
|> Maybe.map .submatches
|> Maybe.map List.head
|> Maybe.join
|> Maybe.join
in
if Regex.contains (Regex.regex "//Ignore") str then
Just Ignore
else if Regex.contains (Regex.regex "//DefaultValue") str then
Just DefaultValue
else if Regex.contains (Regex.regex "//NoDeclaration") str then
Just NoDeclaration
else if Maybe.isJust submatch then
Maybe.map (\name -> FieldNameConversionApplication name) submatch
else if Regex.contains (Regex.regex "//FieldNameMapping") str then
Just FieldNameConversion
else
Nothing

_ ->
Nothing
Expand Down
5 changes: 4 additions & 1 deletion elm/src/Transformation/Decoders.elm
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,11 @@ recordFieldDec ctx typeName ( name, type_ ) =
typeDecoder ctx =
decodeType ctx type_

nameAlias =
getNameAlias ctx typeName name

appTemplate funcName =
Result.map (Application (Application (Variable <| qualifiedName ctx.decoderPrefix funcName) (String name))) (typeDecoder ctx)
Result.map (Application (Application (Variable <| qualifiedName ctx.decoderPrefix funcName) (String nameAlias))) (typeDecoder ctx)
in
case Dict.get ( typeName, name ) ctx.defaultRecordValues of
Just defaultValue ->
Expand Down
9 changes: 6 additions & 3 deletions elm/src/Transformation/Encoders.elm
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ genEncoderForRecord ctx typeName recordAst =
case recordAst of
TypeRecord l ->
Result.map (Application (Variable <| qualifiedName ctx.decoderPrefix "object"))
(Result.map List <| Result.combine <| List.map (encodeRecordField ctx) l)
(Result.map List <| Result.combine <| List.map (encodeRecordField ctx typeName) l)

TypeConstructor _ _ ->
Result.map (\gen -> Application gen (variable "" "value")) (encodeType ctx recordAst)
Expand All @@ -163,12 +163,15 @@ genEncoderForRecord ctx typeName recordAst =
Err "It is not a record!"


encodeRecordField ctx ( name, type_ ) =
encodeRecordField ctx typeName ( name, type_ ) =
let
typeEncoder ctx =
encodeType ctx type_

nameAlias =
getNameAlias ctx typeName name
in
Result.map (\typeEncoder -> Tuple [ (String name), (Application <| typeEncoder) (Access (variable "" "value") [ name ]) ]) (typeEncoder ctx)
Result.map (\typeEncoder -> Tuple [ (String nameAlias), (Application <| typeEncoder) (Access (variable "" "value") [ name ]) ]) (typeEncoder ctx)


encodeType ctx type_ =
Expand Down
Loading

0 comments on commit a04758b

Please sign in to comment.