77
88from __future__ import annotations
99
10+ import re
1011from numbers import Real
1112from typing import Union , Any , Literal , Annotated
1213
13- from pydantic import BaseModel , ConfigDict , Field , field_validator , SerializeAsAny
14+ from pydantic import BaseModel , ConfigDict , Field , field_validator , model_validator , SerializeAsAny
1415
1516from .csapi4py .constants import GeometryTypes
1617from .api_utils import UCUMCode , URI
1718from .geometry import Geometry
1819
20+ # SWE Common 3 NameToken: basicTypes.json#/$defs/NameToken
21+ _NAME_TOKEN_RE = re .compile (r"^[A-Za-z][A-Za-z0-9_\-]*$" )
22+
23+
24+ def check_named (component , location : str ) -> None :
25+ """Validate that a component bound via SoftNamedProperty carries a NameToken `name`."""
26+ name = getattr (component , "name" , None )
27+ if not name :
28+ raise ValueError (
29+ f"{ location } : a component bound here must carry `name` (SWE Common 3 SoftNamedProperty)."
30+ )
31+ if not _NAME_TOKEN_RE .match (name ):
32+ raise ValueError (
33+ f"{ location } : `name` { name !r} does not match NameToken pattern "
34+ f"^[A-Za-z][A-Za-z0-9_\\ -]*$."
35+ )
36+
37+
1938"""
2039 NOTE: The following classes are used to represent the Record Schemas that are required for use with Datastreams
2140The names are likely to change to include a "Schema" suffix to differentiate them from the actual data structures.
@@ -34,6 +53,11 @@ class AnyComponentSchema(BaseModel):
3453 model_config = ConfigDict (populate_by_name = True )
3554 type : str = Field (...)
3655 id : str = Field (None )
56+ # Wire-format flat carrier for SoftNamedProperty.name. Optional on the component
57+ # itself (per AbstractDataComponent.json); enforced as required by parent
58+ # binding-context validators (DataRecord/DataChoice/Vector/DataArray/Matrix and
59+ # the datastream/controlstream schema wrappers in schema_datamodels.py).
60+ name : str = Field (None )
3761 label : str = Field (None )
3862 description : str = Field (None )
3963 updatable : bool = Field (False )
@@ -43,44 +67,61 @@ class AnyComponentSchema(BaseModel):
4367
4468class DataRecordSchema (AnyComponentSchema ):
4569 type : Literal ["DataRecord" ] = "DataRecord"
46- # `name` is not part of AbstractDataComponent in SWE Common 3 — it belongs to
47- # the SoftNamedProperty wrapper that binds a component as a record field. We
48- # accept it here only because the OSH server emits `name` at the root-level
49- # DataRecord of a datastream's recordSchema/resultSchema. See
50- # docs/osh_spec_deviations.md (root-component-name).
51- name : str = Field (None )
5270 fields : list ["AnyComponent" ] = Field (...)
5371
72+ @model_validator (mode = "after" )
73+ def _fields_require_name (self ):
74+ for i , f in enumerate (self .fields ):
75+ check_named (f , f"DataRecord.fields[{ i } ]" )
76+ return self
77+
5478
5579class VectorSchema (AnyComponentSchema ):
5680 label : str = Field (...)
57- name : str = Field (...)
5881 type : Literal ["Vector" ] = "Vector"
5982 definition : str = Field (...)
6083 reference_frame : str = Field (..., alias = 'referenceFrame' )
6184 local_frame : str = Field (None , alias = 'localFrame' )
6285 # TODO: VERIFY might need to be moved further down when these are defined
6386 coordinates : SerializeAsAny [Union [list [CountSchema ], list [QuantitySchema ], list [TimeSchema ]]] = Field (...)
6487
88+ @model_validator (mode = "after" )
89+ def _coordinates_require_name (self ):
90+ for i , c in enumerate (self .coordinates ):
91+ check_named (c , f"Vector.coordinates[{ i } ]" )
92+ return self
93+
6594
6695class DataArraySchema (AnyComponentSchema ):
6796 type : Literal ["DataArray" ] = "DataArray"
68- name : str = Field (...)
6997 element_count : dict | str | CountSchema = Field (..., alias = 'elementCount' ) # Should type of Count
7098 element_type : "AnyComponent" = Field (..., alias = 'elementType' )
7199 encoding : str = Field (...) # TODO: implement an encodings class
72100 values : list = Field (None )
73101
102+ @model_validator (mode = "after" )
103+ def _element_type_requires_name (self ):
104+ check_named (self .element_type , "DataArray.elementType" )
105+ return self
106+
74107
75108class MatrixSchema (AnyComponentSchema ):
76109 type : Literal ["Matrix" ] = "Matrix"
77110 element_count : dict | str | CountSchema = Field (..., alias = 'elementCount' ) # Should be type of Count
111+ # TODO: spec defines Matrix.elementType as a single component (allOf SoftNamedProperty + AnyComponent),
112+ # not a list. Cardinality fix is out of scope for the name-validator change.
78113 element_type : list ["AnyComponent" ] = Field (..., alias = 'elementType' )
79114 encoding : str = Field (...) # TODO: implement an encodings class
80115 values : list = Field (None )
81116 reference_frame : str = Field (None )
82117 local_frame : str = Field (None )
83118
119+ @model_validator (mode = "after" )
120+ def _element_type_requires_name (self ):
121+ for i , et in enumerate (self .element_type ):
122+ check_named (et , f"Matrix.elementType[{ i } ]" )
123+ return self
124+
84125
85126class DataChoiceSchema (AnyComponentSchema ):
86127 type : Literal ["DataChoice" ] = "DataChoice"
@@ -89,6 +130,12 @@ class DataChoiceSchema(AnyComponentSchema):
89130 choice_value : CategorySchema = Field (..., alias = 'choiceValue' ) # TODO: Might be called "choiceValues"
90131 items : list ["AnyComponent" ] = Field (...)
91132
133+ @model_validator (mode = "after" )
134+ def _items_require_name (self ):
135+ for i , item in enumerate (self .items ):
136+ check_named (item , f"DataChoice.items[{ i } ]" )
137+ return self
138+
92139
93140class GeometrySchema (AnyComponentSchema ):
94141 label : str = Field (...)
@@ -127,7 +174,6 @@ class AnySimpleComponentSchema(AnyComponentSchema):
127174 nil_values : list = Field (None , alias = 'nilValues' )
128175 constraint : Any = Field (None )
129176 value : Any = Field (None )
130- name : str = Field (...)
131177
132178
133179class AnyScalarComponentSchema (AnySimpleComponentSchema ):
0 commit comments