Skip to content

Commit

Permalink
Added golang struct exporter
Browse files Browse the repository at this point in the history
Signed-off-by: Sebastian Schleemilch <[email protected]>
  • Loading branch information
sschleemilch committed Sep 23, 2024
1 parent 4e186f2 commit 298f07c
Show file tree
Hide file tree
Showing 10 changed files with 602 additions and 6 deletions.
66 changes: 66 additions & 0 deletions docs/go.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Go lang struct exporter

This exporter produces type struct definitions for the [go](https://go.dev/) programming language.

## Exporter specific arguments

### `--package`

The name of the package the generated sources (output and types output) will have.

# Example

Input model:
```yaml
# model.vspec
Vehicle:
type: branch
description: Vehicle
Vehicle.Speed:
type: sensor
description: Speed
datatype: uint16
Vehicle.Location:
type: sensor
description: Location
datatype: Types.GPSLocation
```
Input type definitions:
```yaml
# types.vspec
Types:
type: branch
description: Custom Types
Types.GPSLocation:
type: struct
description: GPS Location
Types.GPSLocation.Longitude:
type: property
description: Longitude
datatype: float
Types.GPSLocation.Latitude:
type: property
description: Latitude
datatype: float
```
Generator call:
```bash
vspec export go --vspec model.vspec --types types.vspec --package vss --output vss.go
```

Generated file:
```go
// vss.go
package vss

type Vehicle struct {
Speed uint16
Location GPSLocation
}
type GPSLocation struct {
Longitude float32
Latitude float32
}
```
1 change: 1 addition & 0 deletions docs/vspec.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ vspec export json --vspec spec/VehicleSignalSpecification.vspec --output vss.jso
- [id](./id.md)
- [protobuf](./protobuf.md)
- [samm](./samm.md)
- [go](./go.md)

## Argument Explanations

Expand Down
1 change: 1 addition & 0 deletions src/vss_tools/vspec/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def cli(ctx: click.Context, log_level: str, log_file: Path):
"yaml": "vss_tools.vspec.vssexporters.vss2yaml:cli",
"tree": "vss_tools.vspec.vssexporters.vss2tree:cli",
"samm": "vss_tools.vspec.vssexporters.vss2samm.vss2samm:cli",
"go": "vss_tools.vspec.vssexporters.vss2go:cli",
},
)
@click.pass_context
Expand Down
277 changes: 277 additions & 0 deletions src/vss_tools/vspec/vssexporters/vss2go.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
# Copyright (c) 2024 Contributors to COVESA
#
# This program and the accompanying materials are made available under the
# terms of the Mozilla Public License 2.0 which is available at
# https://www.mozilla.org/en-US/MPL/2.0/
#
# SPDX-License-Identifier: MPL-2.0


from __future__ import annotations

from pathlib import Path

import rich_click as click
from anytree import PreOrderIter

import vss_tools.vspec.cli_options as clo
from vss_tools import log
from vss_tools.vspec.datatypes import Datatypes, is_array
from vss_tools.vspec.main import get_trees
from vss_tools.vspec.model import VSSDataBranch, VSSDataDatatype, VSSDataStruct
from vss_tools.vspec.tree import VSSNode

datatype_map = {
Datatypes.INT8_ARRAY[0]: "[]int8",
Datatypes.INT16_ARRAY[0]: "[]int16",
Datatypes.INT32_ARRAY[0]: "[]int32",
Datatypes.INT64_ARRAY[0]: "[]int64",
Datatypes.UINT8_ARRAY[0]: "[]uint8",
Datatypes.UINT16_ARRAY[0]: "[]uint16",
Datatypes.UINT32_ARRAY[0]: "[]uint32",
Datatypes.UINT64_ARRAY[0]: "[]uint64",
Datatypes.FLOAT[0]: "float32",
Datatypes.FLOAT_ARRAY[0]: "[]float32",
Datatypes.DOUBLE[0]: "float64",
Datatypes.DOUBLE_ARRAY[0]: "[]float64",
Datatypes.STRING_ARRAY[0]: "[]string",
Datatypes.NUMERIC[0]: "float64",
Datatypes.NUMERIC_ARRAY[0]: "[]float64",
Datatypes.BOOLEAN_ARRAY[0]: "[]bool",
Datatypes.BOOLEAN[0]: "bool",
}


class NoInstanceRootException(Exception):
pass


def get_instance_root(root: VSSNode, depth: int = 1) -> tuple[VSSNode, int]:
if root.parent is None:
raise NoInstanceRootException()
if isinstance(root.parent.data, VSSDataBranch):
if root.parent.data.is_instance:
return get_instance_root(root.parent, depth + 1)
else:
return root.parent, depth
else:
raise NoInstanceRootException()


def add_children_map_entries(root: VSSNode, fqn: str, replace: str, map: dict[str, str]) -> None:
child: VSSNode
for child in root.children:
child_fqn = child.get_fqn()
new_name = child_fqn.replace(fqn, replace)
map[child_fqn] = new_name
add_children_map_entries(child, fqn, replace, map)


def get_instance_mapping(root: VSSNode | None) -> dict[str, str]:
if root is None:
return {}
instance_map: dict[str, str] = {}
for node in PreOrderIter(root):
if isinstance(node.data, VSSDataBranch):
if node.data.is_instance:
instance_root, depth = get_instance_root(node)
new_name = instance_root.get_fqn() + "." + "I" + str(depth)
fqn = node.get_fqn()
instance_map[fqn] = new_name
add_children_map_entries(node, fqn, instance_root.get_fqn(), instance_map)
return instance_map


def get_datatype(node: VSSNode) -> str | None:
"""
Gets the datatype string of a node.
"""
datatype = None
if isinstance(node.data, VSSDataDatatype):
if node.data.datatype in datatype_map:
return datatype_map[node.data.datatype]
d = Datatypes.get_type(node.data.datatype)
if d:
datatype = d[0]
# Struct type
d_raw = node.data.datatype
array = is_array(d_raw)
struct_datatype = node.data.datatype.rstrip("[]")
if array:
struct_datatype = f"[]{struct_datatype}"
datatype = struct_datatype
return datatype


class GoStructMember:
def __init__(self, name: str, datatype: str) -> None:
self.name = name
self.datatype = datatype


class GoStruct:
def __init__(self, name: str) -> None:
self.name = name
self.members: list[GoStructMember] = []

def __str__(self) -> str:
r = f"type {self.name.replace('.', '')} struct {{\n"
for member in self.members:
r += f"\t{member.name} {member.datatype.replace('.', '')}\n"
r += "}\n"
return r

def __eq__(self, other: object) -> bool:
if not isinstance(other, GoStruct):
return False
return self.name == other.name


def get_struct_name(fqn: str, map: dict[str, str]) -> str:
if fqn in map:
return map[fqn]
else:
return fqn


def get_go_structs(root: VSSNode | None, map: dict[str, str], type_tree: bool = False) -> dict[str, GoStruct]:
structs: dict[str, GoStruct] = {}
if root is None:
return structs
for node in PreOrderIter(root):
if isinstance(node.data, VSSDataBranch) or isinstance(node.data, VSSDataStruct):
struct = GoStruct(get_struct_name(node.get_fqn(), map))
for child in node.children:
datatype = get_datatype(child)
if not datatype:
datatype = get_struct_name(child.get_fqn(), map)
member = GoStructMember(child.name, datatype)
struct.members.append(member)
if type_tree and isinstance(node.data, VSSDataBranch):
pass
else:
structs[struct.name] = struct
return structs


def get_prefixes(structs: dict[str, GoStruct]) -> list[str]:
prefixes: dict[str, int] = {}
for struct in structs.values():
split = struct.name.split(".")
if len(split) == 1:
continue
prefix = split[0]
if prefix in prefixes:
prefixes[prefix] += 1
else:
prefixes[prefix] = 1
return [p for p in prefixes.keys()]


def get_prefix_strip_conflicts(prefix: str, structs: dict[str, GoStruct]) -> int:
structs_new: list[str] = []
for struct in structs.values():
split = struct.name.split(".")
sp = split[0]
if len(split) == 1:
structs_new.append(struct.name)
else:
if sp != prefix:
structs_new.append(struct.name)
else:
log.debug(f"Stripping, {prefix=}, {struct.name=}")
structs_new.append(".".join(split[1:]))
log.debug(f"New name: {structs_new[-1]}")

return len(structs_new) - len(set(structs_new))


def strip_structs_prefix(prefix: str, structs: dict[str, GoStruct]) -> int:
stripped = 0
for struct in structs.values():
split = struct.name.split(".")
if len(split) > 1:
if split[0] == prefix:
struct.name = ".".join(split[1:])
stripped += 1
for member in struct.members:
dtsplit = member.datatype.split(".")
if len(dtsplit) > 1:
array_member = dtsplit[0].startswith("[]")
content = dtsplit[0].lstrip("[]")
if content == prefix:
member.datatype = ".".join(dtsplit[1:])
if array_member:
member.datatype = "[]" + member.datatype
return stripped


@click.command()
@clo.vspec_opt
@clo.output_required_opt
@clo.include_dirs_opt
@clo.extended_attributes_opt
@clo.strict_opt
@clo.aborts_opt
@clo.overlays_opt
@clo.quantities_opt
@clo.units_opt
@clo.types_opt
@click.option("--package", default="vss", help="Go package name", show_default=True)
@click.option("--short-names/--no-short-names", default=True, show_default=True, help="Shorten struct names")
def cli(
vspec: Path,
output: Path,
include_dirs: tuple[Path],
extended_attributes: tuple[str],
strict: bool,
aborts: tuple[str],
overlays: tuple[Path],
quantities: tuple[Path],
units: tuple[Path],
types: tuple[Path],
package: str,
short_names: bool,
):
"""
Export as Go structs.
"""
tree, datatype_tree = get_trees(
vspec=vspec,
include_dirs=include_dirs,
aborts=aborts,
strict=strict,
extended_attributes=extended_attributes,
quantities=quantities,
units=units,
types=types,
overlays=overlays,
)
instance_map = get_instance_mapping(tree)
structs = get_go_structs(tree, instance_map)
log.info(f"Structs, amount={len(structs)}")
datatype_structs = get_go_structs(datatype_tree, instance_map, True)
log.info(f"Datatype structs, amount={len(datatype_structs)}")
structs.update(datatype_structs)

if short_names:
rounds = 0
while True:
prefixes = get_prefixes(structs)
log.debug(f"{prefixes=}")
stripped = 0
for prefix in prefixes:
conflicts = get_prefix_strip_conflicts(prefix, structs)
log.debug(f"Struct name conflicts, prefix={prefix}, conflicts={conflicts}")
if conflicts == 0:
stripped += strip_structs_prefix(prefix, structs)
if stripped == 0:
break
else:
rounds += 1
log.info(f"Name stripping, {rounds=}")

with open(output, "w") as f:
f.write(f"package {package}\n\n")
for struct in structs.values():
f.write(str(struct))
15 changes: 15 additions & 0 deletions tests/vspec/test_datatypes/expected.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package vss

type A struct {
UInt8 uint8
Int8 int8
UInt16 uint16
Int16 int16
UInt32 uint16
Int32 int32
UInt64 uint64
Int64 int64
IsBoolean bool
Float float32
Double float64
}
Loading

0 comments on commit 298f07c

Please sign in to comment.