Add functions to create Elm files
parent
8850dea498
commit
6337c2cc03
|
@ -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])
|
|
@ -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
|
Loading…
Reference in New Issue