Skip to main content

humblegen/backend/elm/
encoder_generation.rs

1use super::{field_name, to_atom};
2use crate::ast;
3
4use inflector::Inflector;
5use itertools::Itertools;
6
7/// Generate elm code for encoder functions for `spec`.
8pub fn generate_struct_and_enum_encoders(spec: &ast::Spec) -> String {
9    spec.iter()
10        .filter_map(|spec_item| match spec_item {
11            ast::SpecItem::StructDef(sdef) => {
12                let json_encoder = generate_struct_json_encoder(sdef);
13                let query_encoder = generate_struct_query_encoder(sdef);
14                Some(format!("{}\n\n\n{}", json_encoder, query_encoder))
15            }
16            ast::SpecItem::EnumDef(edef) => Some(generate_enum_encoder(edef)),
17            ast::SpecItem::ServiceDef(_) => None,
18        })
19        .join("\n\n\n")
20}
21
22fn generate_struct_json_encoder(sdef: &ast::StructDef) -> String {
23    let ns = "";
24    format!(
25        "{encoder_name} : {type_name} -> E.Value\n{encoder_name} obj =\n    E.object\n        [ {fields}\n        ]",
26        encoder_name = struct_or_enum_encoder_name(&sdef.name, ns),
27        type_name = sdef.name,
28        fields = sdef.fields.iter().map(|f| generate_field_json_encoder(f, ns)).join("\n        , "),
29    )
30}
31
32fn generate_struct_query_encoder(sdef: &ast::StructDef) -> String {
33    let ns = "";
34    format!(
35        "{encoder_name} : {type_name} -> List Url.Builder.QueryParameter\n{encoder_name} obj =\n    [ {fields}\n    ]",
36        encoder_name = query_struct_encoder_name(&sdef.name, ns),
37        type_name = sdef.name,
38        fields = sdef.fields.iter().map(|f| generate_field_query_encoder(f, ns)).join("\n    , "),
39    )
40}
41
42fn generate_enum_encoder(edef: &ast::EnumDef) -> String {
43    let ns = "";
44
45    format!(
46        "{encoder_name} : {type_name} -> E.Value\n{encoder_name} v =\n    case v of\n        {variants}",
47        encoder_name = struct_or_enum_encoder_name(&edef.name, ns),
48        type_name = edef.name,
49        variants = edef
50            .variants
51            .iter()
52            .map(|v| generate_variant_encoder_branch(v, ns))
53            .join("\n        "),
54    )
55}
56
57fn generate_field_json_encoder(field: &ast::FieldNode, ns: &str) -> String {
58    format!(
59        "(\"{name}\", {value_encoder} obj.{field_name})",
60        name = field.pair.name,
61        field_name = field_name(&field.pair.name),
62        value_encoder = generate_type_json_encoder(&field.pair.type_ident, ns)
63    )
64}
65
66fn generate_field_query_encoder(field: &ast::FieldNode, ns: &str) -> String {
67    // TODO: escape strings (but we could fix this in the whole codebase)
68    match field.pair.type_ident {
69        ast::TypeIdent::BuiltIn(ast::AtomType::Str) => format!(
70            "Url.Builder.string \"{name}\" obj.{field_name}",
71            name = field.pair.name,
72            field_name = field_name(&field.pair.name)
73        ),
74        ast::TypeIdent::BuiltIn(ast::AtomType::Uuid) => format!(
75            "Url.Builder.string \"{name}\" (BuiltinUuid.encodeQuery obj.{field_name})",
76            name = field.pair.name,
77            field_name = field_name(&field.pair.name)
78        ),
79        ast::TypeIdent::BuiltIn(ast::AtomType::Bytes) => format!(
80            "Url.Builder.string \"{name}\" (BuiltinBytes.encodeQuery obj.{field_name})",
81            name = field.pair.name,
82            field_name = field_name(&field.pair.name)
83        ),
84        ast::TypeIdent::BuiltIn(ast::AtomType::I32)
85        | ast::TypeIdent::BuiltIn(ast::AtomType::U32)
86        | ast::TypeIdent::BuiltIn(ast::AtomType::U8) => format!(
87            "Url.Builder.int \"{name}\" obj.{field_name}",
88            name = field.pair.name,
89            field_name = field_name(&field.pair.name),
90        ),
91        _ => {
92            // encode other types as json encoded strings
93            format!(
94                "obj.{field_name} |> {value_encoder} |> E.encode 4 |> Url.Builder.string \"{name}\"",
95                name = field.pair.name,
96                field_name = field_name(&field.pair.name),
97                value_encoder = generate_complex_type_query_encoder(&field.pair.type_ident, ns)
98            )
99        }
100    }
101}
102
103fn generate_variant_encoder_branch(variant: &ast::VariantDef, ns: &str) -> String {
104    match variant.variant_type {
105        ast::VariantType::Simple => format!("{name} -> E.string \"{name}\"", name = variant.name),
106        ast::VariantType::Tuple(ref tdef) => format!(
107            "{name} {field_names} -> E.object [ (\"{name}\", E.list identity [{field_encoders}]) ]",
108            name = variant.name,
109            field_names = (0..tdef.elements().len())
110                .map(|i| format!("x{}", i))
111                .join(" "),
112            field_encoders = tdef
113                .elements()
114                .iter()
115                .enumerate()
116                .map(|(idx, component)| format!(
117                    "{} x{}",
118                    generate_type_json_encoder(component, ns),
119                    idx
120                ))
121                .join(", "),
122        ),
123        ast::VariantType::Struct(ref fields) => format!(
124            "{name} obj -> E.object [ (\"{name}\", E.object [{fields}]) ]",
125            name = variant.name,
126            fields = fields
127                .iter()
128                .map(|f| generate_field_json_encoder(f, ns))
129                .join(", "),
130        ),
131        ast::VariantType::Newtype(ref ty) => format!(
132            "{name} obj -> E.object [ (\"{name}\", {enc} obj) ]",
133            name = variant.name,
134            enc = generate_type_json_encoder(ty, ns),
135        ),
136    }
137}
138
139/// Generate elm code for a type encoder.
140fn generate_type_encoder(
141    atom_encoder: &dyn Fn(&ast::AtomType, &str) -> String,
142    type_ident: &ast::TypeIdent,
143    ns: &str,
144) -> String {
145    match type_ident {
146        ast::TypeIdent::BuiltIn(atom) => atom_encoder(atom, ns),
147        ast::TypeIdent::List(inner) => {
148            format!("E.list {}", to_atom(generate_type_json_encoder(inner, ns)))
149        }
150        ast::TypeIdent::Option(inner) => format!(
151            "builtinEncodeMaybe {}",
152            to_atom(generate_type_json_encoder(inner, ns))
153        ),
154        ast::TypeIdent::Result(ok, err) => format!(
155            "builtinEncodeResult {} {}",
156            to_atom(generate_type_json_encoder(err, ns)),
157            to_atom(generate_type_json_encoder(ok, ns))
158        ),
159        ast::TypeIdent::Map(key, value) => {
160            assert_eq!(
161                generate_type_json_encoder(key, ns),
162                "E.string",
163                "can only encode string keys in maps"
164            );
165            format!(
166                "E.dict identity {}",
167                to_atom(generate_type_json_encoder(value, ns))
168            )
169        }
170        ast::TypeIdent::Tuple(tdef) => generate_tuple_encoder(tdef, ns),
171        ast::TypeIdent::UserDefined(ident) => struct_or_enum_encoder_name(ident, ns),
172    }
173}
174
175pub(crate) fn generate_type_json_encoder(type_ident: &ast::TypeIdent, ns: &str) -> String {
176    generate_type_encoder(&generate_atom_json_encoder, type_ident, ns)
177}
178
179fn generate_complex_type_query_encoder(type_ident: &ast::TypeIdent, ns: &str) -> String {
180    generate_type_encoder(&generate_atom_query_encoder, type_ident, ns)
181}
182
183pub(crate) fn generate_type_urlcomponent_encoder(type_ident: &ast::TypeIdent, ns: &str) -> String {
184    generate_type_encoder(&generate_atom_urlcomponent_encoder, type_ident, ns)
185}
186
187fn generate_atom_json_encoder(atom: &ast::AtomType, ns: &str) -> String {
188    match atom {
189        ast::AtomType::Empty => "(_ -> E.null)".to_owned(),
190        ast::AtomType::Str => "E.string".to_owned(),
191        ast::AtomType::I32 => "E.int".to_owned(),
192        ast::AtomType::U32 => "E.int".to_owned(),
193        ast::AtomType::U8 => "E.int".to_owned(),
194        ast::AtomType::F64 => "E.float".to_owned(),
195        ast::AtomType::Bool => "E.bool".to_owned(),
196        ast::AtomType::DateTime => format!("{}builtinEncodeIso8601", ns),
197        ast::AtomType::Date => format!("{}builtinEncodeDate", ns),
198        ast::AtomType::Uuid => "BuiltinUuid.encode".to_owned(),
199        ast::AtomType::Bytes => "BuiltinBytes.encode".to_owned(),
200    }
201}
202
203fn generate_atom_query_encoder(atom: &ast::AtomType, ns: &str) -> String {
204    match atom {
205        ast::AtomType::Empty => "E.null".to_owned(),
206        ast::AtomType::Str => "Url.Builder.string".to_owned(),
207        ast::AtomType::Uuid => "Url.Builder.uuid".to_owned(),
208        ast::AtomType::Bytes => "Url.Builder.bytes".to_owned(),
209        ast::AtomType::I32 | ast::AtomType::U32 | ast::AtomType::U8 => "Url.Builder.int".to_owned(),
210        ast::AtomType::F64 => "E.float".to_owned(),
211        ast::AtomType::Bool => "E.bool".to_owned(),
212        ast::AtomType::DateTime => format!("{}builtinEncodeIso8601", ns),
213        ast::AtomType::Date => format!("{}builtinEncodeDate", ns),
214    }
215}
216
217fn generate_atom_urlcomponent_encoder(atom: &ast::AtomType, ns: &str) -> String {
218    match atom {
219        ast::AtomType::Empty => unimplemented!(),
220        ast::AtomType::Str => "identity".to_owned(),
221        ast::AtomType::I32 | ast::AtomType::U32 | ast::AtomType::U8 => "String.fromInt".to_owned(),
222        ast::AtomType::F64 => "String.fromFloat".to_owned(),
223        ast::AtomType::Bool => "String.fromBool".to_owned(),
224        ast::AtomType::DateTime => format!("{}builtinEncodeIso8601", ns),
225        ast::AtomType::Date => format!("{}builtinEncodeDate", ns),
226        ast::AtomType::Uuid => "BuiltinUuid.encodeUrlcomponent".to_owned(),
227        ast::AtomType::Bytes => "BuiltinBytes.encodeUrlcomponent".to_owned(),
228    }
229}
230
231fn generate_tuple_encoder(tdef: &ast::TupleDef, ns: &str) -> String {
232    format!(
233        "\\({field_names}) -> E.list identity [ {encode_values} ]",
234        field_names = (0..tdef.elements().len())
235            .map(|i| format!("x{}", i))
236            .join(", "),
237        encode_values = tdef
238            .elements()
239            .iter()
240            .enumerate()
241            .map(|(idx, component)| format!(
242                "{} x{}",
243                generate_type_json_encoder(component, ns),
244                idx
245            ))
246            .join(", "),
247    )
248}
249
250/// Construct name of encoder function for specific `ident`.
251pub(crate) fn struct_or_enum_encoder_name(ident: &str, ns: &str) -> String {
252    format!("{}encode{}", ns, ident.to_pascal_case())
253}
254
255pub(crate) fn query_encoder(ident: &ast::TypeIdent, ns: &str) -> String {
256    // TODO: should narrow type of query parameter. According to spec query has to be a user defined struct
257    if let ast::TypeIdent::UserDefined(query_ty_name) = ident {
258        query_struct_encoder_name(query_ty_name, ns)
259    } else {
260        panic!("query MUST be a user defined struct");
261    }
262}
263
264pub(crate) fn query_struct_encoder_name(ident: &str, ns: &str) -> String {
265    format!("{}buildQuery{}", ns, ident.to_pascal_case())
266}