Source code for syside.json

  1"""
  2Convenience module intending to match the standard library ``json`` module.
  3"""
  4
  5from collections.abc import Iterable
  6from typing import Callable, overload
  7import typing
  8from uuid import UUID
  9from warnings import warn
 10from dataclasses import dataclass
 11
 12from .._loading import BaseModel, Environment
 13from .. import core as syside
 14
 15
[docs] 16@dataclass 17class SerializationError(Exception): 18 """ 19 Error serializing element to SysML v2 JSON. 20 """ 21 22 report: syside.SerdeReport[syside.Element]
23 24 25type DeserializationReport = syside.SerdeReport[ 26 syside.Element | str | syside.DocumentSegment 27] 28 29
[docs] 30class SerdeError(Exception): 31 """ 32 Class for exceptions from serialization and deserialization 33 """
34 35
[docs] 36@dataclass 37class DeserializationError(SerdeError): 38 """ 39 Error deserializing document from SysML v2 JSON. 40 """ 41 42 model: syside.DeserializedModel 43 report: DeserializationReport 44 45 def __str__(self) -> str: 46 return str(self.report)
47 48
[docs] 49@dataclass 50class ProjectDeserializationError(SerdeError): 51 """ 52 Error deserializing project from SysML v2 JSON. 53 """ 54 55 models: list[syside.DeserializedModel] 56 reports: list[DeserializationReport] 57 58 def __str__(self) -> str: 59 return "\n".join(str(report) for report in self.reports)
60 61
[docs] 62class SerdeWarning(Warning): 63 """ 64 Class for warnings from serialization and deserialization 65 """
66 67
[docs] 68def dumps( 69 element: syside.Element, 70 options: syside.SerializationOptions, 71 indent: int = 2, 72 use_spaces: bool = True, 73 final_new_line: bool = True, 74 include_cross_ref_uris: bool = True, 75) -> str: 76 """ 77 Serialize ``element`` to a SysML v2 JSON ``str``. 78 79 See the documentation of the :py:class:`SerializationOptions 80 <syside.SerializationOptions>` class for documentation of the possible 81 options. The options object constructed with 82 :py:meth:`SerializationOptions.minimal 83 <syside.SerializationOptions.minimal>` instructs to produce a minimal JSON 84 without any redundant elements that results in significantly smaller JSONs. 85 Examples of redundant information that is avoided using minimal 86 configuration are: 87 88 + including fields for null values; 89 + including fields whose values match the default values; 90 + including redefined fields that are duplicates of redefining fields; 91 + including derived fields that can be computed from minimal JSON (for 92 example, the result value of evaluating an expression); 93 + including implied relationships. 94 95 .. note:: 96 97 Syside does not construct all derived properties yet. Therefore, setting 98 ``options.include_derived`` to ``True`` may result in a JSON that does 99 not satisfy the schema. 100 101 :param element: 102 The SysML v2 element to be serialized to SysML v2 JSON. 103 :param options: 104 The serialization options to use when serializing SysML v2 to JSON. 105 :param indent: 106 How many space or tab characters to use for indenting the JSON. 107 :param use_spaces: 108 Whether use spaces or tabs for indentation. 109 :param final_new_line: 110 Whether to add a newline character at the end of the generated string. 111 :param include_cross_ref_uris: 112 Whether to add potentially relative URIs as ``@uri`` property to 113 references of Elements from documents other than the one owning 114 ``element``. Note that while such references are non-standard, they 115 match the behaviour of XMI exports in Pilot implementation which use 116 relative URIs for references instead of plain element IDs. 117 :return: 118 ``element`` serialized as JSON. 119 """ 120 121 writer = syside.JsonStringWriter( 122 indent=indent, 123 use_spaces=use_spaces, 124 final_new_line=final_new_line, 125 include_cross_ref_uris=include_cross_ref_uris, 126 ) 127 128 report = syside.serialize(element, writer, options) 129 130 if not report: 131 raise SerializationError(report) 132 133 for msg in report.messages: 134 if msg.severity == syside.DiagnosticSeverity.Warning: 135 warn(msg.message, category=SerdeWarning, stacklevel=2) 136 137 return writer.result
138 139 140type JsonSourceNew = tuple[str | syside.Url, str] 141type JsonSourceInto = tuple[syside.Document, str] 142 143 144def _deserialize_document( 145 s: str, 146 document: str | syside.Url | syside.Document, 147 reader: syside.JsonReader, 148 attributes: syside.AttributeMap | None = None, 149) -> tuple[ 150 syside.DeserializedModel, 151 DeserializationReport, 152 syside.AttributeMap, 153]: 154 new_doc: syside.SharedMutex[syside.Document] | None = None 155 if isinstance(document, str): 156 document = syside.Url(document) 157 if isinstance(document, syside.Url): 158 ext = document.path.rsplit(".", 1)[-1].lower() 159 if ext == "sysml": 160 lang = syside.ModelLanguage.SysML 161 elif ext == "kerml": 162 lang = syside.ModelLanguage.KerML 163 else: 164 raise ValueError(f"Unknown document language, could not infer from '{ext}'") 165 new_doc = syside.Document.create_st(url=document, language=lang) 166 with new_doc.lock() as doc: 167 document = doc # appease pylint 168 169 with reader.bind(s) as json: 170 if attributes is None: 171 attributes = json.attribute_hint() 172 if attributes is None: 173 raise ValueError("Cannot deserialize model with unmapped attributes") 174 175 model, report = syside.deserialize(document, json, attributes) 176 177 return model, report, attributes 178 179 180_STDLIB_MAP: syside.IdMap | None = None 181 182 183def _map_from_env(docs: Iterable[syside.SharedMutex[syside.Document]]) -> syside.IdMap: 184 ids = syside.IdMap() 185 for mutex in docs: 186 with mutex.lock() as doc: 187 ids.insert_or_assign(doc) 188 return ids 189 190 191def _loads_project( 192 s: Iterable[JsonSourceNew | JsonSourceInto], 193 environment: Environment | None = None, 194 resolve: Callable[[str, UUID], syside.Element | None] | None = None, 195 attributes: syside.AttributeMap | None = None, 196) -> tuple[ 197 BaseModel, 198 list[tuple[syside.DeserializedModel, DeserializationReport]], 199]: 200 reader = syside.JsonReader() 201 202 init: list[tuple[syside.DeserializedModel, DeserializationReport]] = [] 203 for doc, src in s: 204 model, report, attributes = _deserialize_document(src, doc, reader, attributes) 205 init.append((model, report)) 206 207 passed = all(report.passed() for _, report in init) 208 if not passed: 209 raise ProjectDeserializationError( 210 models=[model for model, _ in init], reports=[report for _, report in init] 211 ) 212 213 for _, report in init: 214 for msg in report.messages: 215 if ( 216 msg.severity == syside.DiagnosticSeverity.Warning 217 and not msg.message.startswith("Could not find reference") 218 ): 219 warn(msg.message, category=SerdeWarning, stacklevel=3) 220 221 base: syside.IdMap | None = None 222 if environment is None or environment is getattr(Environment, "_STDLIB", None): 223 environment = Environment.get_default() 224 # ignore pylint, we use global variable for caching 225 global _STDLIB_MAP # pylint: disable=global-statement 226 if _STDLIB_MAP is None: 227 _STDLIB_MAP = _map_from_env(environment.documents) 228 base = _STDLIB_MAP 229 elif not resolve: 230 # defer environment map creation as it may be relatively expensive and 231 # duplicate user provided ``resolve`` 232 base = _map_from_env(environment.documents) 233 234 local: syside.IdMap | None = None 235 if len(init) > 1: 236 # small optimization as 1 document will have resolved its own references 237 # automatically 238 local = syside.IdMap() 239 for model, _ in init: 240 local.insert_or_assign(model.document) 241 242 def resolver(url: str, uuid: UUID) -> syside.Element | None: 243 if local and (result := local(url, uuid)): 244 return result 245 246 if resolve and (result := resolve(url, uuid)): 247 return result 248 249 nonlocal base 250 if base is None: 251 base = _map_from_env(environment.documents) 252 253 if result := base(url, uuid): 254 return result 255 256 return None 257 258 index = environment.index() 259 for i, (model, _) in enumerate(init): 260 init[i] = (model, model.link(resolver)[0]) 261 syside.collect_exports(model.document) 262 index.insert(model.document) 263 264 passed = all(report.passed() for _, report in init) 265 if not passed: 266 raise ProjectDeserializationError( 267 models=[model for model, _ in init], reports=[report for _, report in init] 268 ) 269 270 return BaseModel( 271 result=None, 272 environment=environment, 273 documents=[model.document.mutex for model, _ in init], 274 lib=environment.lib, 275 index=index, 276 ), init 277 278 279def _loads_document( 280 s: str, 281 document: syside.Document | syside.Url | str, 282 attributes: syside.AttributeMap | None = None, 283) -> ( 284 syside.DeserializedModel 285 | tuple[syside.DeserializedModel, syside.SharedMutex[syside.Document]] 286): 287 reader = syside.JsonReader() 288 289 model, report, _ = _deserialize_document(s, document, reader, attributes) 290 291 if not report: 292 raise DeserializationError(model, report) 293 294 for msg in report.messages: 295 if msg.severity == syside.DiagnosticSeverity.Warning: 296 warn(msg.message, category=SerdeWarning, stacklevel=3) 297 298 if not isinstance(document, syside.Document): 299 return model, model.document.mutex 300 return model 301 302 303@overload 304def loads( 305 s: str, 306 document: syside.Document, 307 attributes: syside.AttributeMap | None = None, 308) -> syside.DeserializedModel: 309 """ 310 Deserialize a model from ``s`` into an already existing ``document``. 311 312 Root node will be inferred as: 313 314 1. The first ``Namespace`` (not subtype) without an owning relationship. 315 2. The first ``Element`` that has no serialized owning related element or owning relationship, 316 starting from the first element in the JSON array, and following owning elements up. 317 3. The first element in the array otherwise. 318 319 :param s: 320 The string contained serialized SysML model in JSON array. 321 :param document: 322 The document the model will be deserialized into. 323 :param attributes: 324 Attribute mapping of ``s``. If none provided, this will attempt to infer 325 a corresponding mapping or raise a ``ValueError``. 326 :return: 327 Model deserialized from JSON array. Note that references into other 328 documents will not be resolved, users will need to resolve them by 329 calling ``link`` on the returned model. See also :py:class:`IdMap 330 <syside.IdMap>`. 331 """ 332 333 334@overload 335def loads( 336 s: str, 337 document: syside.Url | str, 338 attributes: syside.AttributeMap | None = None, 339) -> tuple[syside.DeserializedModel, syside.SharedMutex[syside.Document]]: 340 """ 341 Create a new ``document`` and deserialize a model from ``s`` into it. 342 343 Root node will be inferred as: 344 345 1. The first ``Namespace`` (not subtype) without an owning relationship. 346 2. The first ``Element`` that has no serialized owning related element or owning relationship, 347 starting from the first element in the JSON array, and following owning elements up. 348 3. The first element in the array otherwise. 349 350 :param s: 351 The string contained serialized SysML model in JSON array. 352 :param document: 353 A URI in the form of :py:class:`Url <syside.Url>` or a string, new 354 document will be created with. If URI path has no extension, or the 355 extension does not match ``sysml`` or ``kerml``, ``ValueError`` is 356 raised. 357 :param attributes: 358 Attribute mapping of ``s``. If none provided, this will attempt to infer 359 a corresponding mapping or raise a ``ValueError``. 360 :return: 361 Model deserialized from JSON array and the newly created document. Note that 362 references into other documents will not be resolved, users will need to 363 resolve them by calling ``link`` on the returned model. See also 364 :py:class:`IdMap <syside.IdMap>`. 365 """ 366 367 368@overload 369def loads( 370 s: Iterable[JsonSourceNew | JsonSourceInto], 371 /, 372 environment: Environment | None = None, 373 resolve: Callable[[str, UUID], syside.Element | None] | None = None, 374 attributes: syside.AttributeMap | None = None, 375) -> tuple[ 376 BaseModel, 377 list[tuple[syside.DeserializedModel, DeserializationReport]], 378]: 379 """ 380 Deserialize a project of multiple documents from ``s``. 381 382 This is effectively calling ``loads(src, document, attributes) for document, 383 src in s`` and performing the link step afterwards. See also other overloads 384 of ``loads``. 385 386 :param s: 387 Projects sources to deserialize from. If providing a URL string or a 388 ``Url``, new documents will be created for corresponding sources, 389 otherwise deserialization will be performed into the provided 390 ``Documents``. 391 :param environment: 392 ``Environment`` this project depends on. Defaults to the bundled 393 standard library. The ``environment`` will be used to attempt to resolve 394 missing references in the deserialized project. 395 :param resolve: 396 User-provided reference resolution callback that takes priority over 397 ``environment``. See :py:meth:`DeserializedModel.link 398 <syside.DeserializedModel.link>` for more details. 399 :param attributes: 400 Attribute mapping of ``s``. If none provided, this will attempt to infer 401 a corresponding mapping or raise a ``ValueError``. 402 :return: 403 A tuple of project deserialized from JSON sources, and deserialization 404 results 405 :raises ProjectDeserializationError: 406 If either the deserialization or the reference resolution had errors. 407 """ 408 409
[docs] 410def loads( 411 s: str | Iterable[JsonSourceNew | JsonSourceInto], 412 *args: typing.Any, 413 **kwargs: typing.Any, 414) -> ( 415 syside.DeserializedModel 416 | tuple[syside.DeserializedModel, syside.SharedMutex[syside.Document]] 417 | tuple[ 418 BaseModel, 419 list[tuple[syside.DeserializedModel, DeserializationReport]], 420 ] 421): 422 """loads implementation""" 423 if isinstance(s, str): 424 return _loads_document(s, *args, **kwargs) 425 return _loads_project(s, *args, **kwargs)