Skip to content

Commit

Permalink
vsc_ast: use python dataclasses for AST
Browse files Browse the repository at this point in the history
* Implement vehicle service catalog AST using python dataclasses.
* Implement parsing of YAML vsc format into vsc AST format.

Signed-off-by: Mikhail Tsukerman <mikhail.tcukerman@daimler.com>
Signed-off-by: Magnus Feuer <magnus.feuer@mercedes-benz.com>
  • Loading branch information
miketsukerman authored and gunnarx committed Sep 21, 2022
1 parent 3b004df commit deb95e9
Showing 1 changed file with 212 additions and 0 deletions.
212 changes: 212 additions & 0 deletions vsc/model/vsc_ast.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# SPDX-FileCopyrightText: Copyright (c) 2022 MBition GmbH.
# All rights reserved.
# SPDX-License-Identifier: MPL-2.0

from dataclasses import dataclass
from typing import List, Optional, Dict, Any
import yaml


@dataclass
class Argument:
name: str
datatype: str
description: Optional[str] = None
arraysize: Optional[str] = None
range: Optional[str] = None


@dataclass
class Error:
datatype: str
description: Optional[str] = None
arraysize: Optional[str] = None
range: Optional[str] = None


@dataclass
class Method:
name: str
description: Optional[str] = None
error: Optional[List[Error]] = None
input: Optional[List[Argument]] = None
output: Optional[List[Argument]] = None

def __post_init__(self):
if self.error is not None:
self.error = [Error(**e) if isinstance(e, dict) else e for e in self.error]
if self.input is not None:
self.input = [Argument(**a) if isinstance(a, dict) else a for a in self.input]
if self.output is not None:
self.output = [Argument(**a) if isinstance(a, dict) else a for a in self.output]


@dataclass
class Event:
name: str
description: Optional[str] = None
input: Optional[List[Argument]] = None

def __post_init__(self):
if self.input is not None:
self.input = [Argument(**a) if isinstance(a, dict) else a for a in self.input]


@dataclass
class Property:
name: str
datatype: str
description: Optional[str] = None
arraysize: Optional[int] = None


@dataclass
class Member:
name: str
datatype: str
description: Optional[str] = None
arraysize: Optional[str] = None


@dataclass
class Option:
name: str
value: str
description: Optional[str] = None


@dataclass
class Enumeration:
name: str
datatype: str
options: List[Option]
description: Optional[str] = None

def __post_init__(self):
if self.options is not None:
self.options = [Option(**o) if isinstance(o, dict) else o for o in self.options]


@dataclass
class Struct:
name: str
# TODO: do we need type field in a struct?
type: Optional[str] = None
description: Optional[str] = None
members: Optional[List[Member]] = None

def __post_init__(self):
if self.members is not None:
self.members = [Member(**m) if isinstance(m, dict) else m for m in self.members]


@dataclass
class Typedef:
name: str
datatype: str
description: Optional[str] = None
arraysize: Optional[int] = None
min: Optional[int] = None
max: Optional[int] = None


@dataclass
class Include:
file: str
description: Optional[str] = None


@dataclass
class Namespace:
name: str
description: Optional[str] = None

major_version: Optional[int] = None
minor_version: Optional[int] = None

events: Optional[List[Event]] = None
methods: Optional[List[Method]] = None
typedefs: Optional[List[Typedef]] = None
includes: Optional[List[Include]] = None
structs: Optional[List[Struct]] = None
enumerations: Optional[List[Enumeration]] = None
properties: Optional[List[Property]] = None

namespaces: Optional[List['Namespace']] = None

def __post_init__(self):
if self.properties is not None:
self.properties = [Property(**p) if isinstance(p, dict) else p for p in self.properties]
if self.events is not None:
self.events = [Event(**e) if isinstance(e, dict) else e for e in self.events]
if self.methods is not None:
self.methods = [Method(**m) if isinstance(m, dict) else m for m in self.methods]
if self.includes is not None:
self.includes = [Include(**i) if isinstance(i, dict) else i for i in self.includes]
if self.structs is not None:
self.structs = [Struct(**s) if isinstance(s, dict) else s for s in self.structs]
if self.typedefs is not None:
self.typedefs = [Typedef(**t) if isinstance(t, dict) else t for t in self.typedefs]
if self.enumerations is not None:
self.enumerations = [Enumeration(**e) if isinstance(e, dict) else e for e in self.enumerations]
if self.namespaces is not None:
self.namespaces = [Namespace(**n) if isinstance(n, dict) else n for n in self.namespaces]


def parse_dataclass_from_dict(class_name, dictionary):
try:
field_types = class_name.__annotations__
return class_name(
**{f: parse_dataclass_from_dict(field_types[f], dictionary[f]) for f in dictionary})
except AttributeError:
if isinstance(dictionary, (tuple, list)):
return [
parse_dataclass_from_dict(
class_name.__args__[0],
f) for f in dictionary]
return dictionary


def read_yaml_file(filename) -> str:
"""
Tries to read a file which contains yaml into a string
TODO: can have performance implications when file size is big. We have to consider incremental yaml processing.
:param filename:
:return: file contents as string
"""
with open(filename, 'r') as yaml_file:
return yaml_file.read()


def parse_yaml_file(yaml_string: str) -> Dict[Any, Any]:
"""
Tries to parse yaml into a python dictionary
:param yaml_string: String containing text in YAML format
:return: Dictionary
"""
return yaml.safe_load(yaml_string)


def parse_ast_from_dict(ast_dict: Dict[Any, Any]) -> Namespace:
"""
Tries to parse dictionary and create abstract syntax tree
:param ast_dict: dictionary containing AST (vehicle service catalog)
:return: AST
"""
return parse_dataclass_from_dict(Namespace, ast_dict)


def read_ast_from_yaml_file(filename: str) -> Namespace:
"""
Reads a yaml file and returns AST
:param filename: path to a yaml file
:return: abstract syntax tree (vehicle service catalog)
"""

yaml_string = read_yaml_file(filename)

yaml_dict = parse_yaml_file(yaml_string)

ast = parse_ast_from_dict(yaml_dict)

return ast

0 comments on commit deb95e9

Please sign in to comment.