Skip to content

Commit c714899

Browse files
remove serialization methods from streamable resources that were confused with underlying object representations that are needed for actual wire serialization. Update docs to reflect this.
1 parent be1b51b commit c714899

12 files changed

Lines changed: 2130 additions & 779 deletions

File tree

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
# Class hierarchy
2+
3+
OSHConnect's type system has three roughly-orthogonal trees: the
4+
**user-facing wrappers** (`Node`, `System`, `Datastream`, `ControlStream`),
5+
the **CS API resource models** that those wrappers serialize to/from on the
6+
wire, and the **SWE Common schema components** that describe the shape of
7+
observations and commands.
8+
9+
## Wrapper hierarchy
10+
11+
The wrapper classes are in `streamableresource.py`. `StreamableResource[T]`
12+
is an abstract, generic base — `T` is the underlying pydantic resource
13+
model the wrapper holds (`SystemResource`, `DatastreamResource`, or
14+
`ControlStreamResource`). The base manages the MQTT subscribe/publish
15+
plumbing and inbound/outbound deques common to all three concretions.
16+
17+
```mermaid
18+
classDiagram
19+
direction TB
20+
class Node {
21+
+protocol: str
22+
+address: str
23+
+port: int
24+
+discover_systems()
25+
+add_system()
26+
+get_api_helper() APIHelper
27+
+to_storage_dict() dict
28+
}
29+
class StreamableResource~T~ {
30+
<<abstract>>
31+
+get_streamable_id() UUID
32+
+initialize()
33+
+start()
34+
+stop()
35+
+subscribe_mqtt(topic)
36+
+publish(payload, topic)
37+
+to_storage_dict() dict
38+
}
39+
class System {
40+
+name: str
41+
+urn: str
42+
+datastreams: list~Datastream~
43+
+control_channels: list~ControlStream~
44+
+discover_datastreams()
45+
+add_insert_datastream()
46+
+to_smljson_dict() dict
47+
+to_geojson_dict() dict
48+
}
49+
class Datastream {
50+
+get_id() str
51+
+create_observation()
52+
+observation_to_omjson_dict()
53+
+observation_to_swejson_dict()
54+
}
55+
class ControlStream {
56+
+publish_command()
57+
+publish_status()
58+
+command_to_json_dict()
59+
+command_to_swejson_dict()
60+
}
61+
62+
Node "1" o-- "*" System : owns
63+
System "1" o-- "*" Datastream : owns
64+
System "1" o-- "*" ControlStream : owns
65+
66+
StreamableResource <|-- System
67+
StreamableResource <|-- Datastream
68+
StreamableResource <|-- ControlStream
69+
```
70+
71+
`Node` is intentionally *not* a `StreamableResource` — it's a connection
72+
holder, not a streamable.
73+
74+
## CS API resource models
75+
76+
Pydantic models in `resource_datamodels.py`. Each is what `model_dump(by_alias=True)`
77+
produces a CS API JSON body from, and what `model_validate(data, by_alias=True)`
78+
parses a server response into. The wrapper classes above hold one of these
79+
as `_underlying_resource`.
80+
81+
```mermaid
82+
classDiagram
83+
direction TB
84+
class BaseModel {
85+
<<pydantic>>
86+
+model_dump()
87+
+model_validate()
88+
}
89+
class BaseResource {
90+
+id: str
91+
+name: str
92+
+description: str
93+
+type: str
94+
+links: List~Link~
95+
}
96+
class SystemResource {
97+
+feature_type: str // "PhysicalSystem" or "Feature"
98+
+system_id: str
99+
+uid: str
100+
+label: str
101+
+to_smljson_dict()
102+
+to_geojson_dict()
103+
+from_csapi_dict() classmethod
104+
}
105+
class DatastreamResource {
106+
+ds_id: str
107+
+name: str
108+
+valid_time: TimePeriod
109+
+record_schema: DatastreamRecordSchema
110+
+to_csapi_dict()
111+
+from_csapi_dict() classmethod
112+
}
113+
class ControlStreamResource {
114+
+cs_id: str
115+
+input_name: str
116+
+command_schema: CommandSchema
117+
+to_csapi_dict()
118+
+from_csapi_dict() classmethod
119+
}
120+
class ObservationResource {
121+
+result_time: TimeInstant
122+
+phenomenon_time: TimeInstant
123+
+result: dict
124+
+to_omjson_dict()
125+
+to_swejson_dict()
126+
}
127+
128+
BaseModel <|-- BaseResource
129+
BaseModel <|-- SystemResource
130+
BaseModel <|-- DatastreamResource
131+
BaseModel <|-- ControlStreamResource
132+
BaseModel <|-- ObservationResource
133+
```
134+
135+
The `record_schema` / `command_schema` slots are typed
136+
`SerializeAsAny[DatastreamRecordSchema]` /
137+
`SerializeAsAny[CommandSchema]` so they preserve discriminated-union
138+
polymorphism on dump — see the schema document tree below.
139+
140+
## Schema documents
141+
142+
`schema_datamodels.py` defines the polymorphic schema wrappers that live
143+
inside `DatastreamResource.record_schema` and
144+
`ControlStreamResource.command_schema`. The discriminator is the format
145+
field (`obs_format` or `command_format`).
146+
147+
```mermaid
148+
classDiagram
149+
direction TB
150+
class DatastreamRecordSchema {
151+
<<abstract>>
152+
+obs_format: str
153+
}
154+
class SWEDatastreamRecordSchema {
155+
+obs_format = "application/swe+json"
156+
+encoding: Encoding
157+
+record_schema: AnyComponent
158+
}
159+
class JSONDatastreamRecordSchema {
160+
+obs_format = "application/om+json"
161+
+result_schema: AnyComponent
162+
+parameters_schema: AnyComponent
163+
}
164+
165+
class CommandSchema {
166+
<<abstract>>
167+
+command_format: str
168+
}
169+
class SWEJSONCommandSchema {
170+
+command_format = "application/swe+json"
171+
+encoding: Encoding
172+
+record_schema: AnyComponent
173+
}
174+
class JSONCommandSchema {
175+
+command_format = "application/json"
176+
+params_schema: AnyComponent
177+
+result_schema: AnyComponent
178+
+feasibility_schema: AnyComponent
179+
}
180+
181+
DatastreamRecordSchema <|-- SWEDatastreamRecordSchema
182+
DatastreamRecordSchema <|-- JSONDatastreamRecordSchema
183+
CommandSchema <|-- SWEJSONCommandSchema
184+
CommandSchema <|-- JSONCommandSchema
185+
```
186+
187+
Each variant has a `to_*_dict()` / `from_*_dict()` convenience method
188+
matching its format — see [Serialization](serialization.md).
189+
190+
## SWE Common component union
191+
192+
`swe_components.py` defines the SWE Common Data Model component types as a
193+
discriminated union (`AnyComponent = Annotated[Union[...], Field(discriminator="type")]`).
194+
The `type` literal on each subclass routes pydantic to the right concrete
195+
class on parse.
196+
197+
```mermaid
198+
classDiagram
199+
direction TB
200+
class AnyComponentSchema {
201+
+type: str
202+
+id: str
203+
+name: str
204+
+label: str
205+
+description: str
206+
}
207+
class AnySimpleComponentSchema {
208+
+reference_frame: str
209+
+axis_id: str
210+
+nil_values: list
211+
}
212+
class AnyScalarComponentSchema
213+
class DataRecordSchema {
214+
+type = "DataRecord"
215+
+fields: list~AnyComponent~
216+
}
217+
class VectorSchema {
218+
+type = "Vector"
219+
+reference_frame: str
220+
+coordinates: list~Count|Quantity|Time~
221+
}
222+
class DataArraySchema {
223+
+type = "DataArray"
224+
+element_type: AnyComponent
225+
}
226+
class DataChoiceSchema {
227+
+type = "DataChoice"
228+
+items: list~AnyComponent~
229+
}
230+
class GeometrySchema {
231+
+type = "Geometry"
232+
+srs: str
233+
}
234+
class QuantitySchema {
235+
+type = "Quantity"
236+
+uom: UCUMCode|URI
237+
}
238+
class BooleanSchema {
239+
+type = "Boolean"
240+
}
241+
class CountSchema {
242+
+type = "Count"
243+
}
244+
class TimeSchema {
245+
+type = "Time"
246+
+uom: UCUMCode|URI
247+
}
248+
class TextSchema {
249+
+type = "Text"
250+
}
251+
class CategorySchema {
252+
+type = "Category"
253+
}
254+
255+
AnyComponentSchema <|-- DataRecordSchema
256+
AnyComponentSchema <|-- VectorSchema
257+
AnyComponentSchema <|-- DataArraySchema
258+
AnyComponentSchema <|-- DataChoiceSchema
259+
AnyComponentSchema <|-- GeometrySchema
260+
AnyComponentSchema <|-- AnySimpleComponentSchema
261+
AnySimpleComponentSchema <|-- AnyScalarComponentSchema
262+
AnyScalarComponentSchema <|-- BooleanSchema
263+
AnyScalarComponentSchema <|-- CountSchema
264+
AnyScalarComponentSchema <|-- QuantitySchema
265+
AnyScalarComponentSchema <|-- TimeSchema
266+
AnyScalarComponentSchema <|-- CategorySchema
267+
AnyScalarComponentSchema <|-- TextSchema
268+
```
269+
270+
(Range variants — `CountRangeSchema`, `QuantityRangeSchema`, `TimeRangeSchema`,
271+
`CategoryRangeSchema` — extend `AnySimpleComponentSchema` directly and are
272+
omitted from the diagram for brevity.)
273+
274+
## SoftNamedProperty
275+
276+
The `name` field is *not* a property of any data component itself per SWE
277+
Common 3 — it lives on the `SoftNamedProperty` wrapper that binds a child
278+
into a parent. OSHConnect enforces this via `@model_validator(mode="after")`
279+
on the seven binding contexts: `DataRecord.fields`, `DataChoice.items`,
280+
`Vector.coordinates`, `DataArray.elementType`, `Matrix.elementType`, and
281+
the root recordSchema/resultSchema/parametersSchema of datastream and
282+
control-stream wrappers.
283+
284+
See `tests/test_swe_components.py` for the full validation surface.

0 commit comments

Comments
 (0)