Skip to content

Commit

Permalink
Refactor JSON converter generation for readability and reusability
Browse files Browse the repository at this point in the history
Refactor the compilation of JSON converter functions and modules to improve readability and support reuse in other scenarios where we generate Elm program code from type declarations.
  • Loading branch information
Viir committed Aug 13, 2023
1 parent 836e8bc commit 58d9ae8
Show file tree
Hide file tree
Showing 6 changed files with 303 additions and 155 deletions.
222 changes: 148 additions & 74 deletions implement/elm-time/ElmTime/compile-elm-program/src/CompileElmApp.elm
Original file line number Diff line number Diff line change
Expand Up @@ -730,7 +730,7 @@ mapJsonConvertersModuleText { originalSourceModules, sourceDirs } ( sourceFiles,
(\functionsToReplace ->
let
( appFiles, { generatedModuleName, modulesToImport } ) =
mapAppFilesToSupportJsonConversion
mapAppFilesToSupportJsonConverters
{ generatedModuleNamePrefix = interfaceModuleName
, sourceDirs = sourceDirs
}
Expand Down Expand Up @@ -777,7 +777,7 @@ mapJsonConvertersModuleText { originalSourceModules, sourceDirs } ( sourceFiles,
|> Result.mapError (Elm.Syntax.Node.Node functionToReplace.declarationRange)
)
(addImportsInElmModuleText
(List.map (Tuple.pair >> (|>) Nothing) modulesToImport)
(modulesToImport |> Set.toList |> List.map (Tuple.pair >> (|>) Nothing))
moduleText
|> Result.mapError (Elm.Syntax.Node.Node (syntaxRangeCoveringCompleteString moduleText))
)
Expand All @@ -787,84 +787,48 @@ mapJsonConvertersModuleText { originalSourceModules, sourceDirs } ( sourceFiles,
)


mapAppFilesToSupportJsonConversion :
{-| Combines two transformations of the app files to support JSON converters:
- Exposes the choice type tags from declaring modules where necessary to implement JSON decoders.
- Adds a module containing generated JSON conversion functions for the given list of type annotations and choice types.
-}
mapAppFilesToSupportJsonConverters :
{ generatedModuleNamePrefix : List String
, sourceDirs : SourceDirectories
}
-> List ElmTypeAnnotation
-> Dict.Dict String ElmChoiceTypeStruct
-> AppFiles
-> ( AppFiles, { generatedModuleName : List String, modulesToImport : List (List String) } )
mapAppFilesToSupportJsonConversion { generatedModuleNamePrefix, sourceDirs } typeAnnotationsBeforeDeduplicating choiceTypes appFilesBefore =
-> ( AppFiles, { generatedModuleName : List String, modulesToImport : Set.Set (List String) } )
mapAppFilesToSupportJsonConverters { generatedModuleNamePrefix, sourceDirs } typeAnnotationsBeforeDeduplicating choiceTypes appFilesBefore =
let
modulesToImportForChoiceTypes =
choiceTypes
|> Dict.keys
|> List.map moduleNameFromTypeName
|> Set.fromList
|> Set.toList
|> List.map (String.split ".")
generatedFunctions =
buildJsonConverterFunctionsForMultipleTypes typeAnnotationsBeforeDeduplicating choiceTypes

modulesToImport =
[ [ "Dict" ]
, [ "Set" ]
, [ "Array" ]
, [ "Json", "Decode" ]
, [ "Json", "Encode" ]
, [ "Bytes" ]
, [ "Bytes", "Decode" ]
, [ "Bytes", "Encode" ]
]
++ modulesToImportForChoiceTypes
generatedFunctions.generatedFunctions
|> List.map .modulesToImport
|> List.foldl Set.union Set.empty

generatedModuleModulesToImport =
encodingModuleImportBase64 :: List.map (Tuple.pair >> (|>) Nothing) modulesToImport
encodingModuleImportBase64
:: (modulesToImport
|> Set.toList
|> List.map (Tuple.pair >> (|>) Nothing)
)

appFilesAfterExposingChoiceTypesInModules =
modulesToImportForChoiceTypes
generatedFunctions.modulesToImportForChoiceTypes
|> List.foldl (exposeAllInElmModuleInAppFiles sourceDirs) appFilesBefore

typeAnnotationsFunctions =
typeAnnotationsBeforeDeduplicating
|> listRemoveDuplicates
|> List.map buildJsonConverterFunctionsForTypeAnnotation

typeAnnotationsFunctionsForGeneratedModule =
typeAnnotationsFunctions
|> List.concatMap
(\functionsForType ->
[ functionsForType.encodeFunction, functionsForType.decodeFunction ]
)
|> List.map (\function -> { functionName = function.name, functionText = function.text })

dependenciesFunctions =
choiceTypes
|> Dict.toList
|> List.map
(\( choiceTypeName, choiceType ) ->
jsonConverterFunctionFromChoiceType
{ choiceTypeName = choiceTypeName
, encodeValueExpression = jsonEncodeParamName
, typeArgLocalName = "type_arg"
}
choiceType
)
|> List.concatMap
(\functionsForType ->
[ functionsForType.encodeFunction, functionsForType.decodeFunction ]
)
|> List.map (\function -> { functionName = function.name, functionText = function.text })

functionsForGeneratedModule =
typeAnnotationsFunctionsForGeneratedModule ++ dependenciesFunctions ++ generalSupportingFunctionsTexts

generatedModuleTextWithoutModuleDeclaration =
[ [ generatedModuleModulesToImport
|> List.map importSyntaxTextFromModuleNameAndAlias
|> List.sort
|> String.join "\n"
]
, functionsForGeneratedModule |> List.map .functionText
, generatedFunctions.generatedFunctions |> List.map .functionText
]
|> List.concat
|> List.map String.trim
Expand Down Expand Up @@ -895,11 +859,127 @@ mapAppFilesToSupportJsonConversion { generatedModuleNamePrefix, sourceDirs } typ
in
( appFiles
, { generatedModuleName = generatedModuleName
, modulesToImport = generatedModuleName :: modulesToImport
, modulesToImport = Set.insert generatedModuleName modulesToImport
}
)


type alias GenerateFunctionsFromTypesConfig =
{ generateFromTypeAnnotation : ElmTypeAnnotation -> List GenerateFunctionFromTypeResult
, generateFromChoiceType : ( String, ElmChoiceTypeStruct ) -> List GenerateFunctionFromTypeResult
}


type alias GenerateFunctionFromTypeResult =
{ functionName : String
, functionText : String
, modulesToImport : Set.Set (List String)
}


buildJsonConverterFunctionsForMultipleTypes :
List ElmTypeAnnotation
-> Dict.Dict String ElmChoiceTypeStruct
->
{ generatedFunctions : List GenerateFunctionFromTypeResult
, modulesToImportForChoiceTypes : List (List String)
}
buildJsonConverterFunctionsForMultipleTypes typeAnnotations choiceTypes =
let
defaultModulesToImportForFunction =
[ [ "Dict" ]
, [ "Set" ]
, [ "Array" ]
, [ "Json", "Decode" ]
, [ "Json", "Encode" ]
, [ "Bytes" ]
, [ "Bytes", "Decode" ]
, [ "Bytes", "Encode" ]
]
|> Set.fromList

generatedFunctionsForTypes =
generateFunctionsForMultipleTypes
{ generateFromTypeAnnotation =
buildJsonConverterFunctionsForTypeAnnotation
>> (\functionsForType ->
[ functionsForType.encodeFunction, functionsForType.decodeFunction ]
|> List.map
(\function ->
{ functionName = function.name
, functionText = function.text
, modulesToImport = defaultModulesToImportForFunction
}
)
)
, generateFromChoiceType =
(\( choiceTypeName, choiceType ) ->
jsonConverterFunctionFromChoiceType
{ choiceTypeName = choiceTypeName
, encodeValueExpression = jsonEncodeParamName
, typeArgLocalName = "type_arg"
}
choiceType
)
>> (\functionsForType ->
[ functionsForType.encodeFunction, functionsForType.decodeFunction ]
|> List.map
(\function ->
{ functionName = function.name
, functionText = function.text
, modulesToImport = defaultModulesToImportForFunction
}
)
)
}
typeAnnotations
choiceTypes

generatedFunctions =
generatedFunctionsForTypes.generatedFunctions
++ generalSupportingFunctionsTexts
in
{ generatedFunctions = generatedFunctions
, modulesToImportForChoiceTypes = generatedFunctionsForTypes.modulesToImportForChoiceTypes
}


generateFunctionsForMultipleTypes :
GenerateFunctionsFromTypesConfig
-> List ElmTypeAnnotation
-> Dict.Dict String ElmChoiceTypeStruct
->
{ generatedFunctions : List GenerateFunctionFromTypeResult
, modulesToImportForChoiceTypes : List (List String)
}
generateFunctionsForMultipleTypes config typeAnnotationsBeforeDeduplicating choiceTypes =
let
modulesToImportForChoiceTypes =
choiceTypes
|> Dict.keys
|> List.map moduleNameFromTypeName
|> Set.fromList
|> Set.toList
|> List.map (String.split ".")

generatedFunctionsFromTypeAnnotations =
typeAnnotationsBeforeDeduplicating
|> List.Extra.unique
|> List.concatMap config.generateFromTypeAnnotation

generatedFunctionsFromChoiceTypes =
choiceTypes
|> Dict.toList
|> List.concatMap config.generateFromChoiceType

generatedFunctions =
generatedFunctionsFromTypeAnnotations ++ generatedFunctionsFromChoiceTypes
in
{ generatedFunctions = generatedFunctions
, modulesToImportForChoiceTypes = modulesToImportForChoiceTypes
}


buildJsonConverterFunctionsForTypeAnnotation :
ElmTypeAnnotation
-> { encodeFunction : { name : String, text : String }, decodeFunction : { name : String, text : String } }
Expand Down Expand Up @@ -2360,7 +2440,7 @@ type Declaration
| ChoiceTypeDeclaration Elm.Syntax.Type.Type


generalSupportingFunctionsTexts : List { functionName : String, functionText : String }
generalSupportingFunctionsTexts : List GenerateFunctionFromTypeResult
generalSupportingFunctionsTexts =
(generalSupportingFunctionsTextsWithCommonNamePattern
|> List.concatMap
Expand Down Expand Up @@ -2423,6 +2503,13 @@ jsonDecodeSucceedWhenNotNull valueIfNotNull =
)"""
}
]
|> List.map
(\function ->
{ functionName = function.functionName
, functionText = function.functionText
, modulesToImport = Set.empty
}
)


generalSupportingFunctionsTextsWithCommonNamePattern :
Expand Down Expand Up @@ -4215,19 +4302,6 @@ resultCombineConcatenatingErrors =
(Ok [])


listRemoveDuplicates : List a -> List a
listRemoveDuplicates =
List.foldr
(\item list ->
if list |> List.member item then
list

else
item :: list
)
[]


importSyntaxTextFromModuleNameAndAlias : ( List String, Maybe String ) -> String
importSyntaxTextFromModuleNameAndAlias ( moduleName, maybeAlias ) =
"import "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import CompileElmApp
, filePathFromElmModuleName
, importSyntaxTextFromModuleNameAndAlias
, indentElmCodeLines
, mapAppFilesToSupportJsonConversion
, mapAppFilesToSupportJsonConverters
, mapLocatedInSourceFiles
, modulesToAddForBase64Coding
, updateFileContentAtPath
Expand Down Expand Up @@ -169,15 +169,15 @@ loweredForAppInStateManagementShim sourceDirs stateShimConfig config sourceFiles
( appFiles, generateSerializersResult ) =
sourceFiles
|> addModulesFromTextToAppFiles sourceDirs modulesToAdd
|> mapAppFilesToSupportJsonConversion
|> mapAppFilesToSupportJsonConverters
{ generatedModuleNamePrefix = config.interfaceToHostRootModuleName
, sourceDirs = sourceDirs
}
typeToGenerateSerializersFor
jsonConvertedTypesDependencies

modulesToImport =
generateSerializersResult.modulesToImport
Set.toList generateSerializersResult.modulesToImport
++ stateShimConfig.modulesToImport
++ List.map .moduleName (Dict.values supportingTypes.modules)

Expand Down
2 changes: 1 addition & 1 deletion implement/elm-time/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ namespace ElmTime;

public class Program
{
public static string AppVersionId => "2023-08-11";
public static string AppVersionId => "2023-08-12";

private static int AdminInterfaceDefaultPort => 4000;

Expand Down
4 changes: 2 additions & 2 deletions implement/elm-time/elm-time.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
<TargetFramework>net7.0</TargetFramework>
<RootNamespace>ElmTime</RootNamespace>
<AssemblyName>elm-time</AssemblyName>
<AssemblyVersion>2023.0811.0.0</AssemblyVersion>
<FileVersion>2023.0811.0.0</FileVersion>
<AssemblyVersion>2023.0812.0.0</AssemblyVersion>
<FileVersion>2023.0812.0.0</FileVersion>
<Nullable>enable</Nullable>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>
Expand Down
Loading

0 comments on commit 58d9ae8

Please sign in to comment.