go_away/output/go/
mod.rs

1use std::{fmt, fmt::Write};
2
3use indenter::indented;
4use indoc::writedoc;
5
6mod validate;
7
8use super::tabify;
9use validate::UnionValidate;
10
11pub use crate::types::*;
12
13/// An enum representing the possible top-level types in Golang
14///
15/// This shouldn't be instaniated directly but passed using turbofish operator
16/// to registry_to_output enabling it to write out in Golang
17pub enum GoType<'a> {
18    /// A struct variant
19    Struct(&'a Struct),
20    /// A new type variant
21    NewType(&'a NewType),
22    /// A type alias variant
23    Alias(&'a Alias),
24    /// A simple enum variant (does not contain data)
25    Enum(&'a Enum),
26    /// A union variant (enums with data)
27    Union(&'a Union),
28}
29
30impl fmt::Display for GoType<'_> {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
32        let f = &mut tabify::tabify(f);
33        match self {
34            GoType::Struct(details) => {
35                writeln!(f, "type {} struct {{", details.name)?;
36                for field in &details.fields {
37                    writeln!(indented(f), "{}", GoField(field))?;
38                }
39                writeln!(f, "}}")?;
40            }
41            GoType::NewType(details) => {
42                writeln!(f, "type {} {}", details.name, details.inner.go_type())?;
43            }
44            GoType::Alias(details) => {
45                writeln!(f, "type {} {}", details.name, details.inner.go_type())?;
46            }
47            GoType::Enum(details) => {
48                writeln!(f, "type {} string\n", details.name)?;
49                writeln!(f, "const (")?;
50                for variant in &details.variants {
51                    writeln!(
52                        indented(f),
53                        "{}{} {} = \"{}\"",
54                        details.name,
55                        variant.name,
56                        details.name,
57                        variant.serialized_name
58                    )?;
59                }
60                writeln!(f, ")")?;
61            }
62            GoType::Union(details) => {
63                writeln!(f, "type {} struct {{", details.name)?;
64                for variant in &details.variants {
65                    writeln!(
66                        indented(f),
67                        "{} *{}",
68                        variant.go_name(),
69                        variant.ty.go_type()
70                    )?;
71                }
72                writeln!(f, "}}\n")?;
73                write!(f, "{}", UnionMarshal(details))?;
74                write!(f, "{}", UnionUnmarshal(details))?;
75                write!(f, "{}", UnionValidate(details))?;
76            }
77        }
78
79        Ok(())
80    }
81}
82
83pub struct GoField<'a>(&'a Field);
84
85impl fmt::Display for GoField<'_> {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
87        let details = self.0;
88        write!(
89            f,
90            r#"{} {} `json:"{}"`"#,
91            to_pascal_case(&details.name),
92            details.ty.go_type(),
93            details.serialized_name
94        )
95    }
96}
97
98impl FieldType {
99    fn go_type(&self) -> String {
100        match self {
101            FieldType::Named(type_ref) => type_ref.name().to_string(),
102            FieldType::Optional(inner) => format!("*{}", inner.go_type()),
103            FieldType::List(inner) => format!("[]{}", inner.go_type()),
104            FieldType::Map { key, value } => format!("map[{}]{}", key.go_type(), value.go_type()),
105            FieldType::Primitive(Primitive::String) => "string".to_string(),
106            FieldType::Primitive(Primitive::Float) => "float64".to_string(),
107            FieldType::Primitive(Primitive::Int) => "int".to_string(),
108            FieldType::Primitive(Primitive::Bool) => "bool".to_string(),
109            FieldType::Primitive(Primitive::Time) => "time.Time".to_string(),
110        }
111    }
112}
113
114struct UnionMarshal<'a>(&'a Union);
115struct UnionUnmarshal<'a>(&'a Union);
116
117impl fmt::Display for UnionMarshal<'_> {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        let details = self.0;
120        writedoc!(
121            f,
122            r#"
123                func (self {}) MarshalJSON() ([]byte, error) {{
124                    if err := self.Validate(); err != nil {{
125                        return nil, fmt.Errorf("Validate Failed: %w", err)
126                    }}
127            "#,
128            details.name
129        )?;
130        for variant in details.variants.iter() {
131            let f = &mut indented(f);
132            writeln!(f, "if self.{} != nil {{", variant.go_name())?;
133            match &details.representation {
134                UnionRepresentation::AdjacentlyTagged { tag, content } => {
135                    write!(
136                        indented(f),
137                        "{}",
138                        AdjacentlyTaggedMarshaller {
139                            tag,
140                            content,
141                            variant
142                        }
143                    )?;
144                }
145                UnionRepresentation::InternallyTagged { tag } => {
146                    write!(
147                        indented(f),
148                        "{}",
149                        InternallyTaggedMarshaller { tag, variant }
150                    )?;
151                }
152                _ => todo!("Implement the other tagging enum representations"),
153            }
154            write!(f, "}}")?;
155            write!(f, " else ")?;
156        }
157        writeln!(indented(f), "{{")?;
158        writeln!(
159            indented(f),
160            "\treturn nil, fmt.Errorf(\"No variant was present\")"
161        )?;
162        writeln!(indented(f), "}}")?;
163        writeln!(f, "}}")?;
164
165        Ok(())
166    }
167}
168
169impl UnionVariant {
170    fn go_name(&self) -> String {
171        match (&self.name, &self.ty) {
172            (Some(name), _) => name.clone(),
173            (_, FieldType::Named(type_ref)) => type_ref.name().to_string(),
174            _ => todo!("Variant must be named or named type for now (fix this later)"),
175        }
176    }
177}
178
179struct AdjacentlyTaggedMarshaller<'a> {
180    tag: &'a str,
181    content: &'a str,
182    variant: &'a UnionVariant,
183}
184
185impl fmt::Display for AdjacentlyTaggedMarshaller<'_> {
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        writedoc! {
188            f,
189            r#"
190                output := make(map[string]interface{{}})
191                output["{tag}"] = "{serialized_name}"
192                output["{content}"] = self.{variant_go_name}
193                return json.Marshal(output)
194            "#,
195            tag = self.tag,
196            serialized_name = self.variant.serialized_name,
197            content = self.content,
198            variant_go_name = self.variant.go_name()
199        }
200    }
201}
202
203struct InternallyTaggedMarshaller<'a> {
204    tag: &'a str,
205    variant: &'a UnionVariant,
206}
207
208impl fmt::Display for InternallyTaggedMarshaller<'_> {
209    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210        writedoc! {
211            f,
212            r#"
213                return json.Marshal(struct{{
214                    Tag string `json:"{tag}"`
215                    {variant_type}
216                }}{{
217                    Tag: "{serialized_name}",
218                    {variant_type}: *self.{variant_go_name},
219                }})
220            "#,
221            tag = self.tag,
222            serialized_name = self.variant.serialized_name,
223            variant_go_name = self.variant.go_name(),
224            variant_type = self.variant.ty.go_type()
225        }
226    }
227}
228
229// TODO: Adjacent needs {"t": "tag", "d": data}
230// Internal needs {"type": "tag", ..rest_fields}
231// External needs {"tag": {...}}
232
233impl fmt::Display for UnionUnmarshal<'_> {
234    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235        let details = self.0;
236        writeln!(
237            f,
238            "func (self *{}) UnmarshalJSON(data []byte) error {{",
239            details.name
240        )?;
241        match &details.representation {
242            UnionRepresentation::AdjacentlyTagged { tag, content } => {
243                let f = &mut indented(f);
244                writeln!(
245                    f,
246                    "temp := struct {{\n\tTag string `json:\"{}\"`\n}}{{}}",
247                    tag
248                )?;
249                writeln!(f, "if err := json.Unmarshal(data, &temp); err != nil {{")?;
250                writeln!(f, "\treturn err")?;
251                writeln!(f, "}}")?;
252                for variant in &details.variants {
253                    write!(
254                        f,
255                        "{}",
256                        AdjacentlyTaggedVariantUnmarshaller {
257                            content,
258                            variant,
259                            all_variants: &details.variants
260                        }
261                    )?;
262                }
263                writeln!(f, "{{")?;
264                writeln!(indented(f), "return errors.New(\"Unknown type tag\")")?;
265                writeln!(f, "}}")?;
266                writeln!(f, "return nil")?;
267            }
268            UnionRepresentation::InternallyTagged { tag } => {
269                let f = &mut indented(f);
270                writeln!(
271                    f,
272                    "temp := struct{{\n\tTag string `json:\"{}\"`\n}}{{}}",
273                    tag
274                )?;
275                writeln!(f, "if err := json.Unmarshal(data, &temp); err != nil {{")?;
276                writeln!(f, "\treturn err")?;
277                writeln!(f, "}}")?;
278                for variant in &details.variants {
279                    write!(
280                        f,
281                        "{}",
282                        InternallyTaggedVariantUnmarshaller {
283                            variant,
284                            all_variants: &details.variants
285                        }
286                    )?;
287                }
288                writeln!(f, "{{")?;
289                writeln!(indented(f), "return errors.New(\"Unknown type tag\")")?;
290                writeln!(f, "}}")?;
291                writeln!(f, "return nil")?;
292            }
293
294            _ => todo!("Support other enum representaitons"),
295        }
296        writeln!(f, "}}")?;
297
298        // TODO: Support anything other than Adjacent tagging
299        //todo!("Write UnionUnmarshal")
300        Ok(())
301    }
302}
303
304struct AdjacentlyTaggedVariantUnmarshaller<'a> {
305    content: &'a str,
306    variant: &'a UnionVariant,
307    all_variants: &'a [UnionVariant],
308}
309
310impl fmt::Display for AdjacentlyTaggedVariantUnmarshaller<'_> {
311    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
312        writeln!(f, "if temp.Tag == \"{}\" {{", self.variant.serialized_name)?;
313        writeln!(
314            indented(f),
315            "rv := struct {{\n\tData {} `json:\"{}\"`\n}}{{}}",
316            self.variant.ty.go_type(),
317            self.content,
318        )?;
319        writeln!(
320            indented(f),
321            "if err := json.Unmarshal(data, &rv); err != nil {{"
322        )?;
323        writeln!(indented(f), "\treturn err")?;
324        writeln!(indented(f), "}}")?;
325        writeln!(indented(f), "self.{} = &rv.Data", self.variant.go_name())?;
326        for other_variant in self.all_variants {
327            if other_variant == self.variant {
328                continue;
329            }
330            writeln!(indented(f), "self.{} = nil", other_variant.go_name())?;
331        }
332        write!(f, "}} else ")
333    }
334}
335
336struct InternallyTaggedVariantUnmarshaller<'a> {
337    variant: &'a UnionVariant,
338    all_variants: &'a [UnionVariant],
339}
340
341impl fmt::Display for InternallyTaggedVariantUnmarshaller<'_> {
342    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
343        writedoc!(
344            f,
345            r#"
346            if temp.Tag == "{serialized_name}" {{
347                var rv {go_type}
348                if err := json.Unmarshal(data, &rv); err != nil {{
349                    return err
350                }}
351                self.{go_name} = &rv
352            "#,
353            serialized_name = self.variant.serialized_name,
354            go_type = self.variant.ty.go_type(),
355            go_name = self.variant.go_name()
356        )?;
357        for other_variant in self.all_variants {
358            if other_variant == self.variant {
359                continue;
360            }
361            writeln!(indented(f), "self.{} = nil", other_variant.go_name())?;
362        }
363        write!(f, "}} else ")
364    }
365}
366
367fn to_pascal_case(s: &str) -> String {
368    let mut buf = String::with_capacity(s.len());
369    let mut prev_is_underscore = true;
370    for c in s.chars() {
371        if c == '_' {
372            prev_is_underscore = true;
373        } else if prev_is_underscore {
374            buf.push(c.to_ascii_uppercase());
375            prev_is_underscore = false;
376        } else {
377            buf.push(c.to_ascii_lowercase());
378        }
379    }
380    buf
381}
382
383#[cfg(test)]
384mod tests {
385    use insta::assert_snapshot;
386
387    use super::*;
388    use crate::types::TypeRef;
389
390    #[test]
391    fn test_primitive_structs() {
392        assert_snapshot!(
393            GoType::Struct(&Struct {
394                name: "MyStruct".into(),
395                fields: vec![
396                    Field {
397                        name: "a_string".into(),
398                        serialized_name: "a_string".into(),
399                        ty: FieldType::Primitive(Primitive::String),
400                    },
401                    Field {
402                        name: "an_int".into(),
403                        serialized_name: "renamed_tho".into(),
404                        ty: FieldType::Primitive(Primitive::Int),
405                    },
406                    Field {
407                        name: "a_bool".into(),
408                        serialized_name: "also_renamed".into(),
409                        ty: FieldType::Primitive(Primitive::Bool),
410                    },
411                    Field {
412                        name: "a_float".into(),
413                        serialized_name: "a_float".into(),
414                        ty: FieldType::Primitive(Primitive::Float),
415                    },
416                ],
417            })
418            .to_string(),
419            @r###"
420        type MyStruct struct {
421        	AString string `json:"a_string"`
422        	AnInt int `json:"renamed_tho"`
423        	ABool bool `json:"also_renamed"`
424        	AFloat float64 `json:"a_float"`
425        }
426        "###
427        );
428    }
429
430    #[test]
431    fn test_newtype_output() {
432        assert_snapshot!(GoType::NewType(&NewType {
433            name: "UserId".into(),
434            inner: FieldType::Primitive(Primitive::String),
435        })
436        .to_string(), @"type UserId string
437");
438    }
439
440    #[test]
441    fn test_enum_output() {
442        assert_snapshot!(GoType::Enum(&Enum {
443            name: "FulfilmentType".into(),
444            variants: vec![
445                EnumVariant {
446                    name: "Delivery".into(),
447                    serialized_name: "DELIVERY".into(),
448                },
449                EnumVariant {
450                    name: "Collection".into(),
451                    serialized_name: "COLLECTION".into(),
452                },
453            ],
454        })
455        .to_string(), @r###"
456        type FulfilmentType string
457
458        const (
459        	FulfilmentTypeDelivery FulfilmentType = "DELIVERY"
460        	FulfilmentTypeCollection FulfilmentType = "COLLECTION"
461        )
462        "###);
463    }
464
465    #[test]
466    fn test_adjacently_tagged_union_output() {
467        assert_snapshot!(GoType::Union(&Union {
468            name: "MyUnion".into(),
469            representation: UnionRepresentation::AdjacentlyTagged {
470                tag: "type".into(),
471                content: "data".into(),
472            },
473            variants: vec![
474                UnionVariant {
475                    name: Some("VarOne".into()),
476                    ty: FieldType::Named(TypeRef {
477                        name: "VarOne".into()
478                    }),
479                    serialized_name: "VAR_ONE".into(),
480                },
481                UnionVariant {
482                    name: Some("VarTwo".into()),
483                    ty: FieldType::Named(TypeRef {
484                        name: "VarTwo".into()
485                    }),
486                    serialized_name: "VAR_TWO".into(),
487                }
488            ]
489        })
490        .to_string());
491    }
492
493    #[test]
494    fn test_list_types() {
495        assert_snapshot!(
496            FieldType::List(Box::new(FieldType::Primitive(Primitive::String))).go_type(),
497            @"[]string"
498        );
499    }
500
501    #[test]
502    fn test_map_types() {
503        assert_snapshot!(
504            FieldType::Map{
505                key: Box::new(FieldType::Primitive(Primitive::String)),
506                value: Box::new(FieldType::Primitive(Primitive::Int))
507            }.go_type(),
508            @"map[string]int"
509        );
510    }
511
512    #[test]
513    fn test_option_types() {
514        assert_snapshot!(
515            FieldType::Optional(Box::new(FieldType::Primitive(Primitive::String))).go_type(),
516            @"*string"
517        );
518    }
519}