diff --git a/language/dsl/python/src/__tests__/test_auto_id.py b/language/dsl/python/src/__tests__/test_auto_id.py index 1ff1c36c..28c647a1 100644 --- a/language/dsl/python/src/__tests__/test_auto_id.py +++ b/language/dsl/python/src/__tests__/test_auto_id.py @@ -83,14 +83,14 @@ def test_asset_with_explicit_id(self): assert asset.id == "my_custom_id" def test_asset_with_explicit_id_in_slot(self): - """Test that explicit ID is overridden when asset is placed in a slot""" + """Test that explicit ID isn't overridden when asset is placed in a slot""" parent = Collection(id="parent") child = Text(id="explicit_id") parent.withContent(child) # The ID should be regenerated based on parent context - assert child.id == "parent-content-text" + assert child.id == "explicit_id" def test_single_child_without_parent_id(self): @@ -417,7 +417,7 @@ def test_mixed_explicit_and_auto_ids(self): """Test mixing explicit and auto-generated IDs in hierarchy""" root = Collection(id="root") middle = Collection() # Auto-generated - leaf1 = Text(id="explicit_leaf") # Will be overridden + leaf1 = Text(id="explicit_leaf") # Explicit leaf2 = Action() # Auto-generated root.withContent(middle) @@ -426,7 +426,8 @@ def test_mixed_explicit_and_auto_ids(self): assert root.id == "root" assert middle.id == "root-content-collection" # Even though leaf1 had explicit ID, it gets overridden - assert leaf1.id == "root-content-collection-items-0-text" + assert leaf1.id == "explicit_leaf" assert leaf2.id == "root-content-collection-items-1-action" + diff --git a/language/dsl/python/src/__tests__/test_flow.py b/language/dsl/python/src/__tests__/test_flow.py index 203338e4..66f7c5de 100644 --- a/language/dsl/python/src/__tests__/test_flow.py +++ b/language/dsl/python/src/__tests__/test_flow.py @@ -81,7 +81,7 @@ class TestFlow: def test_instantiation_minimal(self): """Test Flow can be instantiated with minimal required parameters""" - navigation = Navigation(begin="start") + navigation = Navigation(BEGIN="start") flow = Flow(id="test_flow", navigation=navigation) assert flow is not None assert flow.id == "test_flow" @@ -92,7 +92,7 @@ def test_instantiation_minimal(self): def test_instantiation_full(self): """Test Flow can be instantiated with all parameters""" - navigation = Navigation(begin="start") + navigation = Navigation(BEGIN="start") views = [View(id="view1", type="text")] schema = Schema(root=SchemaNode()) data = {"initial": "data"} @@ -116,27 +116,27 @@ def test_instantiation_full(self): def test_id_property_getter(self): """Test id property getter""" - navigation = Navigation(begin="start") + navigation = Navigation(BEGIN="start") flow = Flow(id="test_id", navigation=navigation) assert flow.id == "test_id" def test_id_property_setter(self): """Test id property setter""" - navigation = Navigation(begin="start") + navigation = Navigation(BEGIN="start") flow = Flow(id="initial_id", navigation=navigation) flow.id = "new_id" assert flow.id == "new_id" def test_views_property_getter(self): """Test views property getter""" - navigation = Navigation(begin="start") + navigation = Navigation(BEGIN="start") views = [View(id="view1", type="text"), View(id="view2", type="input")] flow = Flow(id="test", navigation=navigation, views=views) assert flow.views == views def test_views_property_setter(self): """Test views property setter""" - navigation = Navigation(begin="start") + navigation = Navigation(BEGIN="start") flow = Flow(id="test", navigation=navigation) new_views = [View(id="new_view", type="button")] flow.views = new_views @@ -144,14 +144,14 @@ def test_views_property_setter(self): def test_schema_property_getter(self): """Test schema property getter""" - navigation = Navigation(begin="start") + navigation = Navigation(BEGIN="start") schema = Schema(root=SchemaNode()) flow = Flow(id="test", navigation=navigation, schema=schema) assert flow.schema == schema def test_schema_property_setter(self): """Test schema property setter""" - navigation = Navigation(begin="start") + navigation = Navigation(BEGIN="start") flow = Flow(id="test", navigation=navigation) new_schema = Schema(root=SchemaNode()) flow.schema = new_schema @@ -159,14 +159,14 @@ def test_schema_property_setter(self): def test_data_property_getter(self): """Test data property getter""" - navigation = Navigation(begin="start") + navigation = Navigation(BEGIN="start") data = {"test": "data"} flow = Flow(id="test", navigation=navigation, data=data) assert flow.data == data def test_data_property_setter(self): """Test data property setter""" - navigation = Navigation(begin="start") + navigation = Navigation(BEGIN="start") flow = Flow(id="test", navigation=navigation) new_data = {"new": "data"} flow.data = new_data @@ -174,21 +174,21 @@ def test_data_property_setter(self): def test_navigation_property_getter(self): """Test navigation property getter""" - navigation = Navigation(begin="start") + navigation = Navigation(BEGIN="start") flow = Flow(id="test", navigation=navigation) assert flow.navigation == navigation def test_navigation_property_setter(self): """Test navigation property setter""" - initial_nav = Navigation(begin="start") - new_nav = Navigation(begin="end") + initial_nav = Navigation(BEGIN="start") + new_nav = Navigation(BEGIN="end") flow = Flow(id="test", navigation=initial_nav) flow.navigation = new_nav assert flow.navigation == new_nav def test_additional_props_methods(self): """Test additional properties methods""" - navigation = Navigation(begin="start") + navigation = Navigation(BEGIN="start") flow = Flow(id="test", navigation=navigation, custom="value", number=42) assert flow.get_additional_prop("custom") == "value" @@ -205,7 +205,7 @@ def test_additional_props_methods(self): def test_json_serialization_minimal(self): """Test JSON serialization with minimal setup""" - navigation = Navigation(begin="start") + navigation = Navigation(BEGIN="start") flow = Flow(id="test_flow", navigation=navigation) json_str = json.dumps(flow.__dict__, default=lambda o: o.__dict__) @@ -221,7 +221,7 @@ def test_json_serialization_minimal(self): def test_json_serialization_full(self): """Test JSON serialization with all properties""" - navigation = Navigation(begin="start") + navigation = Navigation(BEGIN="start") views = [View(id="view1", type="text")] schema = Schema(root=SchemaNode()) data = {"test": "data"} diff --git a/language/dsl/python/src/__tests__/test_navigation.py b/language/dsl/python/src/__tests__/test_navigation.py index a6d3356e..c3656923 100644 --- a/language/dsl/python/src/__tests__/test_navigation.py +++ b/language/dsl/python/src/__tests__/test_navigation.py @@ -20,7 +20,7 @@ class TestNavigation: def test_instantiation_minimal(self): """Test Navigation can be instantiated with minimal parameters""" - nav = Navigation(begin="start") + nav = Navigation(BEGIN="start") assert nav is not None assert nav.begin == "start" assert nav.flows == {} @@ -28,25 +28,25 @@ def test_instantiation_minimal(self): def test_instantiation_with_flows(self): """Test Navigation can be instantiated with flows""" flow1 = NavigationFlow(start_state="state1") - nav = Navigation(begin="start", flow1=flow1, flow2="simple_flow") + nav = Navigation(BEGIN="start", flow1=flow1, flow2="simple_flow") assert nav.begin == "start" assert nav.get_flow("flow1") == flow1 assert nav.get_flow("flow2") == "simple_flow" def test_begin_property_getter(self): """Test begin property getter""" - nav = Navigation(begin="initial_state") + nav = Navigation(BEGIN="initial_state") assert nav.begin == "initial_state" def test_begin_property_setter(self): """Test begin property setter""" - nav = Navigation(begin="start") + nav = Navigation(BEGIN="start") nav.begin = "new_start" assert nav.begin == "new_start" def test_flow_methods(self): """Test flow getter and setter methods""" - nav = Navigation(begin="start") + nav = Navigation(BEGIN="start") flow = NavigationFlow(start_state="state1") # Test getting non-existent flow @@ -64,71 +64,12 @@ def test_flow_methods(self): def test_json_serialization(self): """Test JSON serialization""" flow = NavigationFlow(start_state="state1") - nav = Navigation(begin="start", test_flow=flow) + nav = Navigation(BEGIN="start", test_flow=flow) - json_str = json.dumps(nav.__dict__, default=lambda o: o.__dict__) + json_str = nav.serialize() assert json_str is not None data = json.loads(json_str) - assert data["_begin"] == "start" - assert "_flows" in data - -class TestNavigationBaseState: - """Test cases for NavigationBaseState class""" - - def test_instantiation_minimal(self): - """Test NavigationBaseState can be instantiated with minimal parameters""" - state = NavigationBaseState(state_type="TEST") - assert state is not None - assert state.state_type == "TEST" - assert state.on_start is None - assert state.on_end is None - - def test_instantiation_full(self): - """Test NavigationBaseState can be instantiated with all parameters""" - exp_obj = ExpressionObject(exp="test_expression") - state = NavigationBaseState( - state_type="FULL", - on_start="start_expr", - on_end=exp_obj, - custom_prop="custom_value" - ) - - assert state.state_type == "FULL" - assert state.on_start == "start_expr" - assert state.on_end == exp_obj - - def test_properties_getters_setters(self): - """Test all property getters and setters""" - state = NavigationBaseState(state_type="TEST") - - # Test state_type - new_type = "NEW_TYPE" - state.state_type = new_type - assert state.state_type == new_type - - # Test on_start - start_expr = ["expr1", "expr2"] - state.on_start = start_expr - assert state.on_start == start_expr - - # Test on_end - end_expr = ExpressionObject(exp="end_expression") - state.on_end = end_expr - assert state.on_end == end_expr - - def test_json_serialization(self): - """Test JSON serialization""" - state = NavigationBaseState( - state_type="TEST", - on_start="start_expr", - ) - - json_str = json.dumps(state.__dict__, default=lambda o: o.__dict__) - assert json_str is not None - data = json.loads(json_str) - assert data["_state_type"] == "TEST" - assert data["_on_start"] == "start_expr" - + assert data["BEGIN"] == "start" class TestNavigationFlowTransitionableState: """Test cases for NavigationFlowTransitionableState class""" @@ -460,9 +401,9 @@ def test_instantiation_minimal(self): flow = NavigationFlow(start_state="initial") assert flow is not None - assert flow.start_state == "initial" - assert flow.on_start is None - assert flow.on_end is None + assert flow.startState == "initial" + assert flow.onStart is None + assert flow.onEnd is None assert flow.states == {} def test_instantiation_with_states(self): @@ -480,8 +421,8 @@ def test_instantiation_with_states(self): end=end_state ) - assert flow.start_state == "view" - assert flow.on_start == "initFlow()" + assert flow.startState == "view" + assert flow.onStart == "initFlow()" assert flow.get_state("view") == view_state assert flow.get_state("end") == end_state @@ -490,18 +431,18 @@ def test_properties(self): flow = NavigationFlow(start_state="start") # Test start_state - flow.start_state = "new_start" - assert flow.start_state == "new_start" + flow.startState = "new_start" + assert flow.startState == "new_start" # Test on_start start_exp = ExpressionObject(exp="startExpression") - flow.on_start = start_exp - assert flow.on_start == start_exp + flow.onStart = start_exp + assert flow.onStart == start_exp # Test on_end end_exp = ["endExpr1", "endExpr2"] - flow.on_end = end_exp - assert flow.on_end == end_exp + flow.onEnd = end_exp + assert flow.onEnd == end_exp def test_state_methods(self): """Test state getter and setter methods""" @@ -535,12 +476,10 @@ def test_json_serialization(self): end=end_state ) - json_str = json.dumps(flow.__dict__, default=lambda o: o.__dict__) + json_str = flow.serialize() assert json_str is not None data = json.loads(json_str) - - assert data["_start_state"] == "view" - assert data["_on_start"] == "init()" - assert "_states" in data - assert len(data["_states"]) == 2 + print(data) + assert data["startState"] == "view" + assert data["onStart"] == "init()" diff --git a/language/dsl/python/src/data.py b/language/dsl/python/src/data.py index 0d52ab64..73dbf879 100644 --- a/language/dsl/python/src/data.py +++ b/language/dsl/python/src/data.py @@ -3,7 +3,7 @@ """ from typing import List, Optional, Union, TypeVar - +from .utils import Serializable T = TypeVar('T', bound=str) # Future: Build out Expression/Binding template functionality once PEP 750 is available @@ -12,7 +12,7 @@ Binding = str BindingRef = str -class ExpressionObject: +class ExpressionObject(Serializable): """An object with an expression in it""" def __init__(self, exp: Optional[Union[str, List[str]]] = None): diff --git a/language/dsl/python/src/flow.py b/language/dsl/python/src/flow.py index 71e80e13..cc09aaea 100644 --- a/language/dsl/python/src/flow.py +++ b/language/dsl/python/src/flow.py @@ -6,11 +6,11 @@ from .navigation import Navigation, NavigationFlowEndState from .schema import Schema from .view import View - +from .utils import Serializable DataModel = Dict[Any, Any] -class FlowResult: +class FlowResult(Serializable): """The data at the end of a flow""" def __init__( @@ -40,7 +40,7 @@ def data(self, value: Optional[Any]) -> None: self._data = value -class Flow(): +class Flow(Serializable): """ The JSON payload for running Player """ diff --git a/language/dsl/python/src/navigation.py b/language/dsl/python/src/navigation.py index de83d503..4dc2a517 100644 --- a/language/dsl/python/src/navigation.py +++ b/language/dsl/python/src/navigation.py @@ -4,55 +4,58 @@ from typing import Any, Dict, Generic, List, Literal, Optional, TypeVar, Union from .data import Expression, ExpressionObject +from .utils import Serializable T = TypeVar('T', bound=str) -class Navigation: +class Navigation(Serializable): """The navigation section of the flow describes a State Machine for the user.""" - def __init__(self, begin: str, **flows: Union[str, 'NavigationFlow']): - self._begin = begin - self._flows: Dict[str, Union[str, 'NavigationFlow']] = flows + _ignored_json_keys = ["begin", "flows"] + + def __init__(self, BEGIN: str, **flows: Union[str, 'NavigationFlow']): + self._BEGIN = BEGIN + self.additional_props: Dict[str, Union[str, 'NavigationFlow']] = flows @property - def begin(self) -> str: + def BEGIN(self) -> str: """The name of the Flow to begin on""" - return self._begin + return self._BEGIN - @begin.setter + @BEGIN.setter def begin(self, value: str) -> None: - self._begin = value + self._BEGIN = value def get_flow(self, name: str) -> Optional[Union[str, 'NavigationFlow']]: """Get a flow by name""" - return self._flows.get(name) + return self.additional_props.get(name) def set_flow(self, name: str, flow: Union[str, 'NavigationFlow']) -> None: """Set a flow""" - self._flows[name] = flow + self.additional_props[name] = flow @property def flows(self) -> Dict[str, Union[str, 'NavigationFlow']]: """Get all flows""" - return self._flows.copy() + return self.additional_props.copy() NavigationFlowTransition = Dict[str, str] -class NavigationBaseState(Generic[T]): +class NavigationBaseState(Generic[T], Serializable): """The base representation of a state within a Flow""" def __init__( self, state_type: T, - on_start: Optional[Union[str, List[str], ExpressionObject]] = None, - on_end: Optional[Union[str, List[str], ExpressionObject]] = None, + onStart: Optional[Union[str, List[str], ExpressionObject]] = None, + onEnd: Optional[Union[str, List[str], ExpressionObject]] = None, **kwargs: Any ): super().__init__() self._state_type = state_type - self._on_start = on_start - self._on_end = on_end + self._onStart = onStart + self._onEnd = onEnd self._additional_props: Dict[str, Any] = kwargs @property @@ -65,22 +68,22 @@ def state_type(self, value: T) -> None: self._state_type = value @property - def on_start(self) -> Optional[Union[str, List[str], ExpressionObject]]: + def onStart(self) -> Optional[Union[str, List[str], ExpressionObject]]: """An optional expression to run when this view renders""" - return self._on_start + return self._onStart - @on_start.setter - def on_start(self, value: Optional[Union[str, List[str], ExpressionObject]]) -> None: - self._on_start = value + @onStart.setter + def onStart(self, value: Optional[Union[str, List[str], ExpressionObject]]) -> None: + self._onStart = value @property - def on_end(self) -> Optional[Union[str, List[str], ExpressionObject]]: + def onEnd(self) -> Optional[Union[str, List[str], ExpressionObject]]: """An optional expression to run before view transition""" - return self._on_end + return self._onEnd - @on_end.setter - def on_end(self, value: Optional[Union[str, List[str], ExpressionObject]]) -> None: - self._on_end = value + @onEnd.setter + def onEnd(self, value: Optional[Union[str, List[str], ExpressionObject]]) -> None: + self._onEnd = value class NavigationFlowTransitionableState(NavigationBaseState[T]): @@ -90,11 +93,11 @@ def __init__( self, state_type: T, transitions: NavigationFlowTransition, - on_start: Optional[Union[str, List[str], ExpressionObject]] = None, - on_end: Optional[Union[str, List[str], ExpressionObject]] = None, + onStart: Optional[Union[str, List[str], ExpressionObject]] = None, + onEnd: Optional[Union[str, List[str], ExpressionObject]] = None, **kwargs: Any ): - super().__init__(state_type, on_start, on_end, **kwargs) + super().__init__(state_type, onStart, onEnd, **kwargs) self._transitions = transitions @property @@ -115,11 +118,11 @@ def __init__( ref: str, transitions: NavigationFlowTransition, attributes: Optional[Dict[str, Any]] = None, - on_start: Optional[Union[str, List[str], ExpressionObject]] = None, - on_end: Optional[Union[str, List[str], ExpressionObject]] = None, + onStart: Optional[Union[str, List[str], ExpressionObject]] = None, + onEnd: Optional[Union[str, List[str], ExpressionObject]] = None, **kwargs: Any ): - super().__init__('VIEW', transitions, on_start, on_end, **kwargs) + super().__init__('VIEW', transitions, onStart, onEnd, **kwargs) self._ref = ref self._attributes = attributes or {} @@ -148,11 +151,11 @@ class NavigationFlowEndState(NavigationBaseState[Literal['END']]): def __init__( self, outcome: str, - on_start: Optional[Union[str, List[str], ExpressionObject]] = None, - on_end: Optional[Union[str, List[str], ExpressionObject]] = None, + onStart: Optional[Union[str, List[str], ExpressionObject]] = None, + onEnd: Optional[Union[str, List[str], ExpressionObject]] = None, **kwargs: Any ): - super().__init__('END', on_start, on_end, **kwargs) + super().__init__('END', onStart, onEnd, **kwargs) self._outcome = outcome @property @@ -175,11 +178,11 @@ def __init__( self, exp: Expression, transitions: NavigationFlowTransition, - on_start: Optional[Union[str, List[str], ExpressionObject]] = None, - on_end: Optional[Union[str, List[str], ExpressionObject]] = None, + onStart: Optional[Union[str, List[str], ExpressionObject]] = None, + onEnd: Optional[Union[str, List[str], ExpressionObject]] = None, **kwargs: Any ): - super().__init__('ACTION', transitions, on_start, on_end, **kwargs) + super().__init__('ACTION', transitions, onStart, onEnd, **kwargs) self._exp = exp @property @@ -203,11 +206,11 @@ def __init__( exp: Expression, await_result: bool, transitions: NavigationFlowTransition, - on_start: Optional[Union[str, List[str], ExpressionObject]] = None, - on_end: Optional[Union[str, List[str], ExpressionObject]] = None, + onStart: Optional[Union[str, List[str], ExpressionObject]] = None, + onEnd: Optional[Union[str, List[str], ExpressionObject]] = None, **kwargs: Any ): - super().__init__('ASYNC_ACTION', transitions, on_start, on_end, **kwargs) + super().__init__('ASYNC_ACTION', transitions, onStart, onEnd, **kwargs) self._exp = exp self._await = await_result @@ -244,11 +247,11 @@ def __init__( self, ref: str, transitions: NavigationFlowTransition, - on_start: Optional[Union[str, List[str], ExpressionObject]] = None, - on_end: Optional[Union[str, List[str], ExpressionObject]] = None, + onStart: Optional[Union[str, List[str], ExpressionObject]] = None, + onEnd: Optional[Union[str, List[str], ExpressionObject]] = None, **kwargs: Any ): - super().__init__('EXTERNAL', transitions, on_start, on_end, **kwargs) + super().__init__('EXTERNAL', transitions, onStart, onEnd, **kwargs) self._ref = ref @property @@ -268,11 +271,11 @@ def __init__( self, ref: str, transitions: NavigationFlowTransition, - on_start: Optional[Union[str, List[str], ExpressionObject]] = None, - on_end: Optional[Union[str, List[str], ExpressionObject]] = None, + onStart: Optional[Union[str, List[str], ExpressionObject]] = None, + onEnd: Optional[Union[str, List[str], ExpressionObject]] = None, **kwargs: Any ): - super().__init__('FLOW', transitions, on_start, on_end, **kwargs) + super().__init__('FLOW', transitions, onStart, onEnd, **kwargs) self._ref = ref @property @@ -296,9 +299,11 @@ def ref(self, value: str) -> None: ] -class NavigationFlow: +class NavigationFlow(Serializable): """A state machine in the navigation""" + _ignored_json_keys = ["states"] + def __init__( self, start_state: str, @@ -306,47 +311,47 @@ def __init__( on_end: Optional[Union[str, List[str], ExpressionObject]] = None, **states: NavigationFlowState ): - self._start_state = start_state - self._on_start = on_start - self._on_end = on_end - self._states: Dict[str, NavigationFlowState] = states + self._startState = start_state + self._onStart = on_start + self._onEnd = on_end + self.additional_props: Dict[str, NavigationFlowState] = states @property - def start_state(self) -> str: + def startState(self) -> str: """The first state to kick off the state machine""" - return self._start_state + return self._startState - @start_state.setter - def start_state(self, value: str) -> None: - self._start_state = value + @startState.setter + def startState(self, value: str) -> None: + self._startState = value @property - def on_start(self) -> Optional[Union[str, List[str], ExpressionObject]]: + def onStart(self) -> Optional[Union[str, List[str], ExpressionObject]]: """An optional expression to run when this Flow starts""" - return self._on_start + return self._onStart - @on_start.setter - def on_start(self, value: Optional[Union[str, List[str], ExpressionObject]]) -> None: - self._on_start = value + @onStart.setter + def onStart(self, value: Optional[Union[str, List[str], ExpressionObject]]) -> None: + self._onStart = value @property - def on_end(self) -> Optional[Union[str, List[str], ExpressionObject]]: + def onEnd(self) -> Optional[Union[str, List[str], ExpressionObject]]: """An optional expression to run when this Flow ends""" - return self._on_end + return self._onEnd - @on_end.setter - def on_end(self, value: Optional[Union[str, List[str], ExpressionObject]]) -> None: - self._on_end = value + @onEnd.setter + def onEnd(self, value: Optional[Union[str, List[str], ExpressionObject]]) -> None: + self._onEnd = value def get_state(self, name: str) -> Optional[NavigationFlowState]: """Get a state by name""" - return self._states.get(name) + return self.additional_props.get(name) def set_state(self, name: str, state: NavigationFlowState) -> None: """Set a state""" - self._states[name] = state + self.additional_props[name] = state @property def states(self) -> Dict[str, NavigationFlowState]: """Get all states""" - return self._states.copy() + return self.additional_props.copy() diff --git a/language/dsl/python/src/schema.py b/language/dsl/python/src/schema.py index d7f37ecb..fa36a33f 100644 --- a/language/dsl/python/src/schema.py +++ b/language/dsl/python/src/schema.py @@ -4,10 +4,11 @@ from typing import Any, Dict, Generic, Optional, List, TypeVar, Union from .validation import Reference +from .utils import Serializable T = TypeVar('T', bound=str) -class SchemaNode: +class SchemaNode(Serializable): """A Node describes a specific object in the tree""" def __init__(self, **properties: 'SchemaDataTypes'): self._properties: Dict[str, 'SchemaDataTypes'] = properties @@ -23,7 +24,7 @@ def properties(self) -> Dict[str, 'SchemaDataTypes']: return self._properties.copy() -class SchemaDataType(Generic[T]): +class SchemaDataType(Generic[T], Serializable): """Each prop in the object can have a specific DataType""" def __init__( self, @@ -77,7 +78,7 @@ def default(self, value: Optional[T]) -> None: self._default = value -class SchemaRecordType(SchemaDataType[T]): +class SchemaRecordType(SchemaDataType[T], Serializable): """Determines if the Datatype is a record object""" def __init__( self, @@ -99,7 +100,7 @@ def is_record(self, value: bool) -> None: self._is_record = value -class SchemaArrayType(SchemaDataType[T]): +class SchemaArrayType(SchemaDataType[T], Serializable): """Determines if the DataType is an Array Object""" def __init__( self, @@ -125,7 +126,7 @@ def is_array(self, value: bool) -> None: SchemaDataTypes = Union[SchemaDataType[Any], SchemaRecordType[Any], SchemaArrayType[Any]] -class Schema: +class Schema(Serializable): """The Schema organizes all content related to Data and it's types""" def __init__(self, root: SchemaNode, **additional_nodes: SchemaNode): self._root = root @@ -148,7 +149,7 @@ def additional_nodes(self) -> Dict[str, SchemaNode]: """Get all additional nodes""" return self._additional_nodes.copy() -class LanguageDataTypeRef: +class LanguageDataTypeRef(Serializable): """ Helper to compliment `Schema.DataType` to provide a way to export a reference to a data type instead of the whole object @@ -165,7 +166,7 @@ def type(self, value: str) -> None: # Formatting namespace classes -class FormattingReference: +class FormattingReference(Serializable): """A reference to a specific formatter""" def __init__(self, type: str, **kwargs: Any): self._type = type diff --git a/language/dsl/python/src/utils.py b/language/dsl/python/src/utils.py index 10a9333b..fda0c9f6 100644 --- a/language/dsl/python/src/utils.py +++ b/language/dsl/python/src/utils.py @@ -1,8 +1,6 @@ """ Common Serialization Utility """ - -from types import NoneType from json import dumps def isPrivateProperty(string: str): @@ -23,6 +21,18 @@ def _default_json_encoder(obj): else: return lambda o: o.__dict__ +def isEmptyDictionary(obj): + """ + checks if object is an empty dictionary + """ + return isinstance(obj, dict) and obj == {} + +def shouldIgnoreKey(key, obj, ignored_keys): + """ + checks if a key should be ignored during serialization + """ + return isInternalMethod(key) or key in ignored_keys or isEmptyDictionary(obj) + class Serializable(): """ Base class to allow for custom JSON serialization @@ -30,22 +40,34 @@ class Serializable(): # Map of properties that aren't valid Python properties to their serialized value _propMap: dict[str, str] = {} # Types that should be handled by the base serialization logic - _jsonable = (int, list, str, dict, NoneType) + _jsonable = (int, list, str, dict) # Keys that should be ignored during serialization - _ignored_json_keys = ['_propMap', '_ignored_json_keys', '_parent', "_slot_name", "_slot_index"] + _ignored_json_keys = { + '_propMap', + '_ignored_json_keys', + '_parent', + "_slot_name", + "_slot_index", + "_static_id" + } def _serialize(self): _dict = dict() for attr in dir(self): value = getattr(self, attr) key = attr - if isInternalMethod(attr) or key in getattr(self, "_ignored_json_keys", []): + if key == "_ignored_json_keys": + continue + elif shouldIgnoreKey(key, value, getattr(self, "_ignored_json_keys", [])): continue + elif key in ["additional_props", "additional_nodes"] and isinstance(value, dict): + for subKey, subVal in value.items(): + _dict[subKey] = subVal elif isinstance(value, (self._jsonable, Serializable)) or hasattr(value, 'to_dict'): if self._propMap.get(key, None) is not None: key = self._propMap[key] elif(isPrivateProperty(attr) and not isInternalMethod(attr)): - key = attr.replace("_", "") + key = attr.replace("_", "", 1) _dict[key] = value else: @@ -59,7 +81,7 @@ def serialize(self, **kw): indent = kw.pop("indent", 4) # use indent key if passed otherwise 4. _ignored_json_keys = kw.pop("ignored_keys", []) if _ignored_json_keys: - self._ignored_json_keys += _ignored_json_keys + self._ignored_json_keys.update(set(_ignored_json_keys)) # type: ignore return dumps(self, indent=indent, default=_default_json_encoder, **kw) diff --git a/language/dsl/python/src/view.py b/language/dsl/python/src/view.py index f6ce93f6..148cf012 100644 --- a/language/dsl/python/src/view.py +++ b/language/dsl/python/src/view.py @@ -19,22 +19,31 @@ class Slotable(Serializable): def _withSlot(self, name: str, obj: Any, wrapInAssetWrapper: bool = True, isArray = False): val = obj + if val is None: + return self + if wrapInAssetWrapper: if isArray: val = [] for index, asset in enumerate(obj): wrapped = AssetWrapper(asset) if not isAssetWrapperOrSwitch(asset) else asset # Set parent relationship and generate ID for the asset - actual_asset = wrapped.asset if isinstance(wrapped, AssetWrapper) else None + actual_asset = wrapped.asset if isinstance(wrapped, AssetWrapper) else asset if actual_asset and isinstance(actual_asset, Asset): actual_asset._setParent(self, name, index) #pylint: disable=protected-access val.append(wrapped) else: val = AssetWrapper(obj) if not isAssetWrapperOrSwitch(obj) else obj # Set parent relationship and generate ID for the asset - actual_asset = val.asset if isinstance(val, AssetWrapper) else None + actual_asset = val.asset if isinstance(val, AssetWrapper) else obj if actual_asset and isinstance(actual_asset, Asset): actual_asset._setParent(self, name, None) #pylint: disable=protected-access + else: + if isArray: + for index, asset in enumerate(obj): + asset._setParent(self, name, index) #pylint: disable=protected-access + else: + obj._setParent(self, name) #pylint: disable=protected-access self[name] = val return self @@ -48,6 +57,7 @@ class Asset(Slotable): _parent: Optional[Slotable] _slot_name: Optional[str] _slot_index: Optional[int] + _static_id: bool = False def __init__(self, id: Optional[str], type: str) -> None: self.type = type @@ -59,6 +69,7 @@ def __init__(self, id: Optional[str], type: str) -> None: self.id = self._generateID() else: self.id = id + self._static_id = True def _setParent(self, parent: Slotable, slot_name: str, slot_index: Optional[int]): """ @@ -67,8 +78,9 @@ def _setParent(self, parent: Slotable, slot_name: str, slot_index: Optional[int] self._parent = parent self._slot_name = slot_name self._slot_index = slot_index - # Regenerate ID based on parent context - self.id = self._generateID() + if not self._static_id: + # Regenerate ID based on parent context + self.id = self._generateID() def _generateID(self) -> str: """ diff --git a/language/generators/python/src/__tests__/test_generator.py b/language/generators/python/src/__tests__/test_generator.py index 18970746..62b725f1 100644 --- a/language/generators/python/src/__tests__/test_generator.py +++ b/language/generators/python/src/__tests__/test_generator.py @@ -42,8 +42,6 @@ def test_generate_action(self): "skipValidation": True, "role": "test" }, - "accessibility": None, - "label": None, } , sort_keys=True, indent=4) diff --git a/language/generators/python/src/generator.py b/language/generators/python/src/generator.py index 2606f00f..c0209666 100644 --- a/language/generators/python/src/generator.py +++ b/language/generators/python/src/generator.py @@ -45,7 +45,8 @@ PLAYER_DSL_PACKAGE, clean_property_name, generate_class_name, - ast_to_source + ast_to_source, + generate_merged_class_name ) def generate_python_classes( @@ -447,16 +448,32 @@ def _generate_init_method(self, object_type: ObjectType, is_asset: bool) -> ast. for prop_info in properties_info: if is_primitive_const(prop_info.node): continue - assignment = ast.Assign( - targets=[ - ast.Attribute( - value=COMMON_AST_NODES['self'], - attr=prop_info.clean_name, - ctx=ast.Store() + if self._is_slot(prop_info.node): + assignment = ast.Expr( + value=ast.Call( + func=ast.Attribute( + value=ast.Name( + id="self", + ctx=ast.Load() + ), + attr=f"with{prop_info.clean_name.replace('_', '').title()}", + ctx=ast.Load() + ), + args=[ast.Name(id=prop_info.clean_name, ctx=ast.Load())], + keywords=[] ) - ], - value=ast.Name(id=prop_info.clean_name, ctx=ast.Load()) - ) + ) + else: + assignment = ast.Assign( + targets=[ + ast.Attribute( + value=COMMON_AST_NODES['self'], + attr=prop_info.clean_name, + ctx=ast.Store() + ) + ], + value=ast.Name(id=prop_info.clean_name, ctx=ast.Load()) + ) init_def.body.append(assignment) return init_def @@ -876,7 +893,7 @@ def _merge_object_types( # Generate a class name for the merged type merged_class_name = name if name \ - else self._generate_merged_class_name(prop_name, object_types) + else generate_merged_class_name(prop_name, object_types) # Add to classes to generate if not already present if merged_class_name not in self.classes: @@ -886,26 +903,6 @@ def _merge_object_types( # Return AST reference to the merged class return ast.Name(id=merged_class_name, ctx=ast.Load()) - def _generate_merged_class_name(self, base_name: str, object_types: List[NodeType]) -> str: - """Generate a unique class name for merged object types.""" - # Clean the base name - clean_base = clean_property_name(base_name).replace('_', '').title() - - # Try to create a meaningful name from the merged types - type_names = [] - for obj_type in object_types: - if is_named_type(obj_type): - type_names.append(obj_type.name) - elif hasattr(obj_type, 'name') and obj_type.name: - type_names.append(obj_type.name) - - if type_names: - merged_name = ''.join(type_names) + clean_base - else: - merged_name = f"Merged{clean_base}" - - return merged_name - def _handle_intersection_with_unions( self, and_types: List[NodeType], diff --git a/language/generators/python/src/utils.py b/language/generators/python/src/utils.py index b4909626..35494d5d 100644 --- a/language/generators/python/src/utils.py +++ b/language/generators/python/src/utils.py @@ -3,10 +3,10 @@ """ import ast -from typing import NamedTuple +from typing import List, NamedTuple from player_tools_xlr_types.nodes import NodeType - +from player_tools_xlr_types.guards import is_named_type COMMON_AST_NODES = { 'string': ast.Name(id='str', ctx=ast.Load()), @@ -52,3 +52,24 @@ def ast_to_source(module: ast.Module) -> str: node.col_offset = 0 # type: ignore return ast.unparse(module) + + +def generate_merged_class_name(base_name: str, object_types: List[NodeType]) -> str: + """Generate a unique class name for merged object types.""" + # Clean the base name + clean_base = clean_property_name(base_name).replace('_', '').title() + + # Try to create a meaningful name from the merged types + type_names = [] + for obj_type in object_types: + if is_named_type(obj_type): + type_names.append(obj_type.name) + elif hasattr(obj_type, 'name') and obj_type.name: + type_names.append(obj_type.name) + + if type_names: + merged_name = ''.join(type_names) + clean_base + else: + merged_name = f"Merged{clean_base}" + + return merged_name