From 6337c2cc037e7ace9c05658ae6d1fd3fe0376be7 Mon Sep 17 00:00:00 2001 From: Bram van den Heuvel Date: Thu, 5 Jan 2023 06:20:10 +0100 Subject: [PATCH] Add functions to create Elm files --- development/build_objects.py | 360 +++++++++++++++++++++++++++++++++++ development/run_all.py | 12 ++ 2 files changed, 372 insertions(+) create mode 100644 development/build_objects.py create mode 100644 development/run_all.py diff --git a/development/build_objects.py b/development/build_objects.py new file mode 100644 index 0000000..8e2333d --- /dev/null +++ b/development/build_objects.py @@ -0,0 +1,360 @@ +import sys +import time +import yaml + +def main(in_file, out_file): + with open(in_file, 'r', encoding='utf-8') as open_file: + obj = yaml.safe_load(open_file) + OBJECTS = obj['objects'] + NAME = obj['name'] + VERSION = obj['version'].replace('.', '_').capitalize() + + OUT_FILE = out_file + if OUT_FILE.endswith('.elm'): + OUT_FILE = OUT_FILE[:-4] + + encapsulate = lambda s : s if ' ' not in s else '(' + s + ')' + + # Boolean + class BoolField: + @property + def type_name(self): + return 'Bool' + + @property + def encoder(self): + return 'E.bool' + + @property + def decoder(self): + return 'D.bool' + + # Integer + class IntField: + @property + def type_name(self): + return 'Int' + + @property + def encoder(self): + return 'E.int' + + @property + def decoder(self): + return 'D.int' + + # String + class StringField: + @property + def type_name(self): + return 'String' + + @property + def encoder(self): + return 'E.string' + + @property + def decoder(self): + return 'D.string' + + # Float: should be avoided as it isn't allowed for canonical JSON. + class FloatField: + @property + def type_name(self): + return 'Float' + + @property + def encoder(self): + return 'E.float' + + @property + def decoder(self): + return 'D.float' + + # Timestamp + class TimestampField: + @property + def type_name(self): + return 'Timestamp' + + @property + def encoder(self): + return 'encodeTimestamp' + + @property + def decoder(self): + return 'timestampDecoder' + + # JSON Value + class ValueField: + @property + def type_name(self): + return 'E.Value' + + @property + def encoder(self): + return '' + + @property + def decoder(self): + return 'D.value' + + # Enum + class EnumField: + def __init__(self, name) -> None: + self.name = name + + @property + def type_name(self): + return 'Enums.' + self.name + + @property + def encoder(self): + return 'Enums.encode' + self.name + + @property + def decoder(self): + return 'Enums.' + self.name[0].lower() + self.name[1:] + 'Decoder' + + # Another object + class SpecObjectField: + def __init__(self, name) -> None: + self.name = name + + @property + def type_name(self): + return self.name + + @property + def encoder(self): + return 'encode' + self.type_name + + @property + def decoder(self): + decoder_name = self.type_name[0].lower() + self.type_name[1:] + 'Decoder' + + if 'anti_recursion' in OBJECTS[self.name]: + return f'D.lazy (\_ -> {decoder_name})' + else: + return decoder_name + + # List of fields + class ListField: + def __init__(self, child_field): + self.child = child_field + + @property + def type_name(self): + return 'List ' + encapsulate(self.child.type_name) + + @property + def encoder(self): + return 'E.list ' + encapsulate(self.child.encoder) + + @property + def decoder(self): + return 'D.list ' + encapsulate(self.child.decoder) + + # Dict of string -> fields + class DictField: + def __init__(self, child_field): + self.child = child_field + + @property + def type_name(self): + return 'Dict String ' + encapsulate(self.child.type_name) + + @property + def encoder(self): + return 'E.dict identity ' + encapsulate(self.child.encoder) + + @property + def decoder(self): + return 'D.dict ' + encapsulate(self.child.decoder) + + def str_to_field(value : str): + if value.startswith('[') and value.endswith(']'): + return ListField(str_to_field(value[1:-1])) + if value.startswith('{') and value.endswith('}'): + return DictField(str_to_field(value[1:-1])) + if value in OBJECTS: + return SpecObjectField(value) + + match value: + case 'value': + return ValueField() + case 'bool': + return BoolField() + case 'int': + return IntField() + case 'string': + return StringField() + case 'float': + return FloatField() + case 'timestamp': + return TimestampField() + + raise ValueError("Unknown value `" + value + "`") + + class Field: + def __init__(self, key, value): + self.key = key + + self.field = str_to_field(value['type']) + self.required = False + if 'required' in value: + self.required = value['required'] + if not self.required: + self.default = None if 'default' not in value else value['default'] + + @property + def elm_name(self): + if self.key == 'type': + return 'contentType' + else: + words = self.key.lower().replace('_', ' ').replace('.', ' ').split(' ') + words = ''.join([w.capitalize() for w in words]) + words = words[0].lower() + words[1:] + return words + + @property + def encoder(self): + if self.required or self.default is not None: + return ( + 'Just <| ' + self.field.encoder + ' data.' + self.elm_name + ) + elif self.field.__class__ == ValueField: + return 'data.' + self.elm_name + else: + return ( + 'Maybe.map ' + encapsulate(self.field.encoder) + ' data.' + self.elm_name + ) + + @property + def decoder(self): + if self.required: + field = f'D.field "{self.key}"' + elif self.default is None: + field = f'opField "{self.key}"' + else: + field = f'opFieldWithDefault "{self.key}" {self.default}' + + return f'{field} {encapsulate(self.field.decoder)}' + + @property + def type_definition(self): + if self.required or self.default is not None: + return self.field.type_name + else: + return 'Maybe ' + encapsulate(self.field.type_name) + + class Object: + def __init__(self, key, value): + self.name = key + self.description = value['description'] + self.anti_recursion = 'anti_recursion' in value + self.fields = [] + + for k in sorted(value['fields'].keys()): + v = value['fields'][k] + self.fields.append(Field(k, v)) + + @property + def elm_name(self): + if '.' not in self.name: + return self.name + else: + return ''.join([word.capitalize() for word in self.name.split('.')]) + + @property + def lowercase_elm_name(self): + n = self.elm_name + return n[0].lower() + n[1:] + + @property + def encoder_name(self): + return 'encode' + self.elm_name + + @property + def decoder_name(self): + return self.lowercase_elm_name + 'Decoder' + + @property + def encoder(self): + return ( + f"{self.encoder_name} : {self.elm_name} -> E.Value\n" + + f"{self.encoder_name} " + (f'({self.elm_name} data)' if self.anti_recursion else 'data') + " =\n" + + f" maybeObject [\n" + + ',\n'.join(f' ("{f.key}", {f.encoder})' for f in self.fields) + + f"\n ]\n" + + f"\n\n" + ) + + @property + def decoder(self): + return ( + f"{self.decoder_name} : D.Decoder {self.elm_name}\n" + + f"{self.decoder_name} =\n" + + f" D.map{len(self.fields)}\n".replace('D.map1\n', 'D.map\n') + + f" (\\" + ' '.join(["abcdefghijklmnop"[i] for i in range(len(self.fields))]) + ' ->\n' + + f" " + (self.elm_name if self.anti_recursion else '') + " { " + ', '.join([self.fields[i].elm_name + '=' + "abcdefghijklmnop"[i] for i in range(len(self.fields))]) + '})\n' + + ''.join(f" " + encapsulate(f.decoder) + '\n' for f in self.fields) + + f"\n\n" + ) + + @property + def type_definition(self): + return ( + "{-| " + self.description + "\n-}\ntype " + + ('alias' if not self.anti_recursion else '') + f" {self.elm_name} = " + (self.elm_name if self.anti_recursion else "" ) + + " {\n" + ',\n'.join(f" {f.elm_name} : {f.type_definition}" for f in self.fields) + + '\n' + " }\n\n" + ) + + object_list = [Object(name, val) for name, val in OBJECTS.items()] + object_list.sort(key=lambda o : o.elm_name.lower()) + + with open(OUT_FILE + '.elm', 'w') as write_file: + write = write_file.write + + write(f"module {OUT_FILE[4:].replace('/', '.')} exposing (\n ".replace('\\', '.') ) + imports = [f"{o.elm_name + '(..)' if o.anti_recursion else o.elm_name}\n , {o.encoder_name}\n , {o.decoder_name}" for o in object_list] + write('\n , '.join(imports) + "\n )\n") + write("{-| Automatically generated '" + NAME + "'\n\nLast generated at Unix time ") + write(str(int(time.time())) + "\n-}\n\n") + + + content = ''.join([o.type_definition + o.encoder + o.decoder for o in object_list]) + + write("\n") + if 'Dict' in content: + write("import Dict exposing (Dict)\n") + + if 'opField' in content and 'opFieldWithDefault' in content: + write("import Internal.Tools.DecodeExtra exposing (opField, opFieldWithDefault)\n") + elif 'opFieldWithDefault' in content: + write("import Internal.Tools.DecodeExtra exposing (opField)\n") + elif 'opField' in content: + write("import Internal.Tools.DecodeExtra exposing (opField)\n") + + if 'maybeObject' in content: + write("import Internal.Tools.EncodeExtra exposing (maybeObject)\n") + + if 'Timestamp' in content: + write("import Internal.Tools.Timestamp exposing (Timestamp, encodeTimestamp, timestampDecoder)\n") + + if 'Enums' in content: + write("import Internal.Tools.SpecEnums as Enums\n") + + write(""" +import Json.Decode as D +import Json.Encode as E + +""") + + write(content) + + print(f'Generated file {out_file}!') + +if __name__ == '__main__': + main(sys.argv[1], sys.argv[2]) \ No newline at end of file diff --git a/development/run_all.py b/development/run_all.py new file mode 100644 index 0000000..85af670 --- /dev/null +++ b/development/run_all.py @@ -0,0 +1,12 @@ +import os +import build_objects as build + +for path, dirs, files in os.walk('src/'): + for file in files: + if file.endswith('.yaml'): + build.main( + os.path.join(path, file), + os.path.join(path, file[:-5] + '.elm') + ) + + pass