jsonbp 1.2.2__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. {jsonbp-1.2.2/jsonbp.egg-info → jsonbp-1.3.0}/PKG-INFO +3 -2
  2. {jsonbp-1.2.2 → jsonbp-1.3.0}/README.md +1 -1
  3. {jsonbp-1.2.2 → jsonbp-1.3.0}/jsonbp/blueprint.py +128 -8
  4. {jsonbp-1.2.2 → jsonbp-1.3.0}/jsonbp/error.py +7 -4
  5. jsonbp-1.3.0/jsonbp/localization/de_DE.ini +20 -0
  6. jsonbp-1.3.0/jsonbp/localization/fr_FR.ini +20 -0
  7. jsonbp-1.3.0/jsonbp/localization/ru_RU.ini +20 -0
  8. jsonbp-1.3.0/jsonbp/localization/zh_CN.ini +20 -0
  9. {jsonbp-1.2.2 → jsonbp-1.3.0}/jsonbp/parser.py +200 -14
  10. {jsonbp-1.2.2 → jsonbp-1.3.0}/jsonbp/types.py +1 -0
  11. {jsonbp-1.2.2 → jsonbp-1.3.0/jsonbp.egg-info}/PKG-INFO +3 -2
  12. {jsonbp-1.2.2 → jsonbp-1.3.0}/jsonbp.egg-info/SOURCES.txt +13 -9
  13. {jsonbp-1.2.2 → jsonbp-1.3.0}/pyproject.toml +8 -2
  14. {jsonbp-1.2.2 → jsonbp-1.3.0}/tests/test_deserialization.py +1 -1
  15. {jsonbp-1.2.2 → jsonbp-1.3.0}/LICENSE.txt +0 -0
  16. {jsonbp-1.2.2 → jsonbp-1.3.0}/MANIFEST.in +0 -0
  17. {jsonbp-1.2.2 → jsonbp-1.3.0}/jsonbp/__init__.py +0 -0
  18. {jsonbp-1.2.2 → jsonbp-1.3.0}/jsonbp/array.py +0 -0
  19. {jsonbp-1.2.2/jsonbp/localization → jsonbp-1.3.0/jsonbp/basetypes}/__init__.py +0 -0
  20. {jsonbp-1.2.2/jsonbp/types → jsonbp-1.3.0/jsonbp/basetypes}/bool.py +0 -0
  21. {jsonbp-1.2.2/jsonbp/types → jsonbp-1.3.0/jsonbp/basetypes}/date.py +0 -0
  22. {jsonbp-1.2.2/jsonbp/types → jsonbp-1.3.0/jsonbp/basetypes}/decimal.py +0 -0
  23. {jsonbp-1.2.2/jsonbp/types → jsonbp-1.3.0/jsonbp/basetypes}/float.py +0 -0
  24. {jsonbp-1.2.2/jsonbp/types → jsonbp-1.3.0/jsonbp/basetypes}/instant.py +0 -0
  25. {jsonbp-1.2.2/jsonbp/types → jsonbp-1.3.0/jsonbp/basetypes}/integer.py +0 -0
  26. {jsonbp-1.2.2/jsonbp/types → jsonbp-1.3.0/jsonbp/basetypes}/limits.py +0 -0
  27. {jsonbp-1.2.2/jsonbp/types → jsonbp-1.3.0/jsonbp/basetypes}/string.py +0 -0
  28. {jsonbp-1.2.2 → jsonbp-1.3.0}/jsonbp/declaration.py +0 -0
  29. {jsonbp-1.2.2 → jsonbp-1.3.0}/jsonbp/exception.py +0 -0
  30. {jsonbp-1.2.2 → jsonbp-1.3.0}/jsonbp/field.py +0 -0
  31. {jsonbp-1.2.2 → jsonbp-1.3.0}/jsonbp/loader.py +0 -0
  32. {jsonbp-1.2.2/jsonbp/types → jsonbp-1.3.0/jsonbp/localization}/__init__.py +0 -0
  33. {jsonbp-1.2.2 → jsonbp-1.3.0}/jsonbp/localization/en_US.ini +0 -0
  34. {jsonbp-1.2.2 → jsonbp-1.3.0}/jsonbp/localization/pt_BR.ini +0 -0
  35. {jsonbp-1.2.2 → jsonbp-1.3.0}/jsonbp/ply/__init__.py +0 -0
  36. {jsonbp-1.2.2 → jsonbp-1.3.0}/jsonbp/ply/lex.py +0 -0
  37. {jsonbp-1.2.2 → jsonbp-1.3.0}/jsonbp/ply/yacc.py +0 -0
  38. {jsonbp-1.2.2 → jsonbp-1.3.0}/jsonbp.egg-info/dependency_links.txt +0 -0
  39. {jsonbp-1.2.2 → jsonbp-1.3.0}/jsonbp.egg-info/top_level.txt +0 -0
  40. {jsonbp-1.2.2 → jsonbp-1.3.0}/setup.cfg +0 -0
  41. {jsonbp-1.2.2 → jsonbp-1.3.0}/tests/test_caching.py +0 -0
  42. {jsonbp-1.2.2 → jsonbp-1.3.0}/tests/test_choose_root.py +0 -0
  43. {jsonbp-1.2.2 → jsonbp-1.3.0}/tests/test_error_fallbacks.py +0 -0
  44. {jsonbp-1.2.2 → jsonbp-1.3.0}/tests/test_loading.py +0 -0
  45. {jsonbp-1.2.2 → jsonbp-1.3.0}/tests/test_parsing.py +0 -0
  46. {jsonbp-1.2.2 → jsonbp-1.3.0}/tests/test_serialization.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jsonbp
3
- Version: 1.2.2
3
+ Version: 1.3.0
4
4
  Summary: Schema based JSON Parser/Serializer
5
5
  Author-email: Gustavo Venturini <gustavo.c.venturini@gmail.com>
6
6
  License-Expression: BSD-3-Clause
@@ -11,6 +11,7 @@ Classifier: Development Status :: 5 - Production/Stable
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: Operating System :: OS Independent
13
13
  Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
15
  Requires-Python: >=3.11
15
16
  Description-Content-Type: text/markdown
16
17
  License-File: LICENSE.txt
@@ -27,7 +28,7 @@ to and from Python based on schemas. While [json-schema][json_schema] and its
27
28
  implementations offer a more mature and widely used technique, a different
28
29
  approach was desired, which led to the development of this library.
29
30
 
30
- jsonbp's design main goals were:
31
+ jsonbp's main design goals were:
31
32
  - schema reuse through import / type system
32
33
  - custom user definable primitive types
33
34
  - built in numeric fixed precision type which deserializes into Python's Decimal
@@ -9,7 +9,7 @@ to and from Python based on schemas. While [json-schema][json_schema] and its
9
9
  implementations offer a more mature and widely used technique, a different
10
10
  approach was desired, which led to the development of this library.
11
11
 
12
- jsonbp's design main goals were:
12
+ jsonbp's main design goals were:
13
13
  - schema reuse through import / type system
14
14
  - custom user definable primitive types
15
15
  - built in numeric fixed precision type which deserializes into Python's Decimal
@@ -28,7 +28,7 @@ unquote = lambda x : unquoted_str(x)
28
28
 
29
29
  class JsonBlueprint:
30
30
  """
31
- Class to serialize Python objects to JSON strings and vice-versa.
31
+ Class to serialize Python objects to JSON strings and vice versa.
32
32
  """
33
33
 
34
34
  def __init__(self, primitive_types):
@@ -38,6 +38,8 @@ class JsonBlueprint:
38
38
  self.derived_types = dict()
39
39
  self.enums = dict()
40
40
  self.objects = dict()
41
+ self.templates = dict()
42
+ self.unions = dict()
41
43
  self.root = None
42
44
 
43
45
  def __str__(self): # pragma: no cover
@@ -90,6 +92,35 @@ class JsonBlueprint:
90
92
  return True, value
91
93
 
92
94
 
95
+ def _validate_union(self, field_name, unionDef, contents):
96
+ if not isinstance(contents, collections.abc.Mapping):
97
+ return False, create_field_error(field_name,
98
+ ErrorType.INVALID_OBJECT)
99
+
100
+ discriminator = unionDef['discriminator']
101
+ if discriminator not in contents:
102
+ return False, create_field_error(field_name,
103
+ ErrorType.MISSING_FIELD, field=discriminator)
104
+
105
+ tag_value = contents[discriminator]
106
+ if isinstance(tag_value, unquoted_str):
107
+ return False, create_field_error(field_name,
108
+ ErrorType.INVALID_ENUM)
109
+
110
+ branch_name = unionDef['branches'].get(tag_value)
111
+ if branch_name is None:
112
+ return False, create_field_error(field_name,
113
+ ErrorType.UNKNOWN_LITERAL)
114
+
115
+ branch_fields = self._find_object_decl(branch_name)
116
+ success, outcome = self._validate_object(field_name, branch_fields, contents)
117
+ if not success:
118
+ return False, outcome
119
+
120
+ outcome[discriminator] = tag_value
121
+ return True, outcome
122
+
123
+
93
124
  def _validate_array(self, field_name, jArray, contents):
94
125
  if not isinstance(contents, collections.abc.Sequence):
95
126
  return False, create_field_error(field_name,
@@ -111,6 +142,10 @@ class JsonBlueprint:
111
142
  element_type = self._find_object_decl(array_type)
112
143
  deserializer = self._validate_object
113
144
 
145
+ if array_kind == FieldType.UNION:
146
+ element_type = self._find_union_decl(array_type)
147
+ deserializer = self._validate_union
148
+
114
149
  result = list()
115
150
  for idx, value in enumerate(contents):
116
151
  if value is None:
@@ -213,6 +248,18 @@ class JsonBlueprint:
213
248
  result[field_name] = processed
214
249
  continue
215
250
 
251
+ if field_kind == FieldType.UNION:
252
+ unionDef = self._find_union_decl(field_type)
253
+ success, outcome = self._validate_union(
254
+ field_name, unionDef,
255
+ retrieved)
256
+
257
+ if not success:
258
+ return False, outcome
259
+
260
+ result[field_name] = outcome
261
+ continue
262
+
216
263
  success, outcome = self._validate_field(
217
264
  field_name, field_type,
218
265
  retrieved)
@@ -264,12 +311,16 @@ class JsonBlueprint:
264
311
  return self._validate_field(None, root_type,
265
312
  root_contents)
266
313
 
314
+ if root_kind == FieldType.UNION:
315
+ unionDef = self._find_union_decl(root_type)
316
+ return self._validate_union(None, unionDef, root_contents)
317
+
267
318
 
268
319
  def deserialize(self, contents):
269
320
  """Attempts to deserialize a JSON string into a Python object.
270
321
 
271
322
  The returned tuple's first element indicates whether the deserialization
272
- was successful. When deserialization succeeds the respective Python data
323
+ was successful. When deserialization succeeds the resulting Python data
273
324
  will be the second element. Conversely, when a problem is found the first
274
325
  element will be False and the second element will be an instance of
275
326
  :class:`DeserializationError`.
@@ -318,6 +369,8 @@ class JsonBlueprint:
318
369
  collected.extend(source.derived_types.keys())
319
370
  collected.extend(source.enums.keys())
320
371
  collected.extend(source.objects.keys())
372
+ collected.extend(source.templates.keys())
373
+ collected.extend(source.unions.keys())
321
374
 
322
375
  return collected
323
376
 
@@ -369,6 +422,38 @@ class JsonBlueprint:
369
422
 
370
423
  return None
371
424
 
425
+
426
+ def _find_union_decl(self, union_name, checked=None):
427
+ if union_name in self.unions:
428
+ return self.unions[union_name]
429
+
430
+ checked = checked or set()
431
+ checked.add(self)
432
+
433
+ for blueprint in self.includes:
434
+ if blueprint not in checked:
435
+ found = blueprint._find_union_decl(union_name, checked)
436
+ if found is not None:
437
+ return found
438
+
439
+ return None
440
+
441
+
442
+ def _find_template_decl(self, template_name, checked=None):
443
+ if template_name in self.templates:
444
+ return self.templates[template_name]
445
+
446
+ checked = checked or set()
447
+ checked.add(self)
448
+
449
+ for blueprint in self.includes:
450
+ if blueprint not in checked:
451
+ found = blueprint._find_template_decl(template_name, checked)
452
+ if found is not None:
453
+ return found
454
+
455
+ return None
456
+
372
457
  #----------------------------------------------------------------------------
373
458
 
374
459
  def choose_root(self, root_type, as_array=False,
@@ -377,15 +462,15 @@ class JsonBlueprint:
377
462
  """Selects or changes the root element of a blueprint.
378
463
 
379
464
  This function allows you to create a derivative JsonBlueprint
380
- instance, overwriting the root type (or selecting one, if none
465
+ instance, overriding the root type (or selecting one, if none
381
466
  is defined). The original source JsonBlueprint instance remains
382
467
  unchanged.
383
468
 
384
469
  Args:
385
470
  root_type (str): name of the type to be used as root.
386
- as_array (bool): whether the chosen root is an array or not of the selected type.
387
- min_array_length (int): if set as array, minimum size that the root must be.
388
- max_array_length (int): if set as array, maximum size that the root can be.
471
+ as_array (bool): whether the root should be an array of the selected type.
472
+ min_array_length (int): if `as_array` is True, the minimum number of elements the root array must contain.
473
+ max_array_length (int): if `as_array` is True, the maximum number of elements the root array can contain.
389
474
 
390
475
  Returns:
391
476
  A new blueprint, with the root selected or replaced.
@@ -399,7 +484,8 @@ class JsonBlueprint:
399
484
  lookups = [
400
485
  (self._find_object_decl, FieldType.OBJECT),
401
486
  (self._find_element_decl, FieldType.SIMPLE),
402
- (self._find_enum_decl, FieldType.ENUM)
487
+ (self._find_enum_decl, FieldType.ENUM),
488
+ (self._find_union_decl, FieldType.UNION),
403
489
  ]
404
490
 
405
491
  found_root = None
@@ -428,11 +514,44 @@ class JsonBlueprint:
428
514
  result.derived_types = self.derived_types
429
515
  result.enums = self.enums
430
516
  result.objects = self.objects
517
+ result.unions = self.unions
431
518
 
432
519
  return result
433
520
 
434
521
  #----------------------------------------------------------------------------
435
522
 
523
+ def _serialize_union(self, union_type, field_name, content):
524
+ unionDef = self._find_union_decl(union_type)
525
+ discriminator = unionDef['discriminator']
526
+
527
+ if not isinstance(content, collections.abc.Mapping):
528
+ raise SerializationException(f"{field_name} needs a dict to serialize")
529
+
530
+ if discriminator not in content:
531
+ raise SerializationException(
532
+ f"{field_name}: missing discriminator field '{discriminator}'")
533
+
534
+ tag_value = content[discriminator]
535
+ branch_name = unionDef['branches'].get(tag_value)
536
+ if branch_name is None:
537
+ raise SerializationException(
538
+ f"{field_name}: unknown tag value '{tag_value}'")
539
+
540
+ serialized = [f'"{discriminator}":"{tag_value}"']
541
+ branch_fields = self._find_object_decl(branch_name)
542
+
543
+ for fname, fdata in branch_fields.items():
544
+ if fname not in content:
545
+ if fdata.optional:
546
+ continue
547
+ raise SerializationException(f"{field_name}: missing field '{fname}'")
548
+
549
+ processed = self._serialize_element(fdata, fname, content[fname])
550
+ serialized.append(f'"{fname}":{processed}')
551
+
552
+ return "{" + ",".join(serialized) + "}"
553
+
554
+
436
555
  def _serialize_element(self, element, elementName, content):
437
556
  contentKind = element.fieldKind
438
557
  contentType = element.fieldType
@@ -440,7 +559,8 @@ class JsonBlueprint:
440
559
  method = {
441
560
  FieldType.OBJECT: JsonBlueprint._serialize_object,
442
561
  FieldType.ENUM: JsonBlueprint._serialize_enum,
443
- FieldType.SIMPLE: JsonBlueprint._serialize_field
562
+ FieldType.SIMPLE: JsonBlueprint._serialize_field,
563
+ FieldType.UNION: JsonBlueprint._serialize_union,
444
564
  } [contentKind]
445
565
 
446
566
  if is_array(element):
@@ -30,8 +30,8 @@ def use_default_language(language):
30
30
 
31
31
  This language will be applied when no languages are
32
32
  specified in the :func:`DeserializationError.localize` method. When
33
- jsonbp starts, this language is set to American English (en-US). If
34
- he language code passed is not available, it'll also fallback to
33
+ jsonbp starts, this language is set to American English (en-US). If
34
+ the language code passed is not available, it will also fall back to
35
35
  American English.
36
36
 
37
37
  Args:
@@ -57,7 +57,7 @@ def load_translation(filename, language):
57
57
 
58
58
  Args:
59
59
  filename (str): path to the ini file.
60
- language (str): language code that that file corresponds to.
60
+ language (str): language code that the file corresponds to.
61
61
 
62
62
  Raises:
63
63
  IOError: When `filename` cannot be opened for reading.
@@ -139,7 +139,7 @@ class DeserializationError:
139
139
  :func:`use_default_language`.
140
140
 
141
141
  Returns:
142
- the localized issue with the JSON string.
142
+ the localized error message describing the issue.
143
143
 
144
144
  """
145
145
 
@@ -149,6 +149,9 @@ class DeserializationError:
149
149
 
150
150
  for candidate in localization_priority:
151
151
  translation = translations.get(candidate)
152
+ if translation is None:
153
+ prefix = candidate + '_'
154
+ translation = next((translations[k] for k in translations if k.startswith(prefix)), None)
152
155
  if translation is not None:
153
156
  break
154
157
 
@@ -0,0 +1,20 @@
1
+
2
+ [Messages]
3
+ JSON_PARSING=Ungültiges JSON, Fehler in Zeile {line}, Spalte {column}: {message}
4
+ VALUE_PARSING=Wert konnte nicht als '{type}' dekodiert werden
5
+ NULL_VALUE=Nullwert
6
+ OUTSIDE_RANGE=Wert {value} liegt außerhalb des erwarteten Bereichs
7
+ INVALID_LENGTH=Länge {length} liegt außerhalb des erwarteten Bereichs
8
+ UNKNOWN_LITERAL=Unbekannter Wert
9
+ INVALID_ENUM=Wert muss eine Zeichenkette sein
10
+ INVALID_FORMAT=Inhalt entspricht nicht dem erwarteten Format
11
+ MISSING_FIELD=Fehlendes Feld '{field}'
12
+ INVALID_ARRAY=Muss eine Liste sein
13
+ INVALID_OBJECT=Muss ein Objekt sein
14
+
15
+ [Prefixes]
16
+ FIELD=Feld '{assignee}'
17
+ OBJECT=Objekt '{assignee}'
18
+ ARRAY=In Liste '{assignee}' bei Index {index}
19
+ ROOT=Auf Wurzelebene
20
+
@@ -0,0 +1,20 @@
1
+
2
+ [Messages]
3
+ JSON_PARSING=JSON invalide, erreur à la ligne {line}, colonne {column}: {message}
4
+ VALUE_PARSING=Impossible de décoder la valeur en '{type}'
5
+ NULL_VALUE=Valeur nulle
6
+ OUTSIDE_RANGE=La valeur {value} est en dehors de la plage attendue
7
+ INVALID_LENGTH=La longueur {length} est en dehors de la plage attendue
8
+ UNKNOWN_LITERAL=Valeur inconnue
9
+ INVALID_ENUM=La valeur doit être une chaîne de caractères
10
+ INVALID_FORMAT=Le contenu n'est pas dans le format attendu
11
+ MISSING_FIELD=Champ manquant '{field}'
12
+ INVALID_ARRAY=Doit être une liste
13
+ INVALID_OBJECT=Doit être un objet
14
+
15
+ [Prefixes]
16
+ FIELD=Champ '{assignee}'
17
+ OBJECT=Objet '{assignee}'
18
+ ARRAY=Dans la liste '{assignee}' à l'index {index}
19
+ ROOT=Au niveau racine
20
+
@@ -0,0 +1,20 @@
1
+
2
+ [Messages]
3
+ JSON_PARSING=Некорректный JSON, ошибка в строке {line}, столбец {column}: {message}
4
+ VALUE_PARSING=Не удалось декодировать значение как '{type}'
5
+ NULL_VALUE=Нулевое значение
6
+ OUTSIDE_RANGE=Значение {value} выходит за пределы ожидаемого диапазона
7
+ INVALID_LENGTH=Длина {length} выходит за пределы ожидаемого диапазона
8
+ UNKNOWN_LITERAL=Неизвестное значение
9
+ INVALID_ENUM=Значение должно быть строкой
10
+ INVALID_FORMAT=Содержимое не соответствует ожидаемому формату
11
+ MISSING_FIELD=Отсутствует поле '{field}'
12
+ INVALID_ARRAY=Должно быть списком
13
+ INVALID_OBJECT=Должно быть объектом
14
+
15
+ [Prefixes]
16
+ FIELD=Поле '{assignee}'
17
+ OBJECT=Объект '{assignee}'
18
+ ARRAY=В списке '{assignee}' по индексу {index}
19
+ ROOT=На корневом уровне
20
+
@@ -0,0 +1,20 @@
1
+
2
+ [Messages]
3
+ JSON_PARSING=无效的JSON,第{line}行第{column}列发生错误:{message}
4
+ VALUE_PARSING=无法将值解码为"{type}"
5
+ NULL_VALUE=空值
6
+ OUTSIDE_RANGE=值{value}超出预期范围
7
+ INVALID_LENGTH=长度{length}超出预期范围
8
+ UNKNOWN_LITERAL=未知值
9
+ INVALID_ENUM=值必须是字符串
10
+ INVALID_FORMAT=内容格式不符合预期
11
+ MISSING_FIELD=缺少字段"{field}"
12
+ INVALID_ARRAY=必须是列表
13
+ INVALID_OBJECT=必须是对象
14
+
15
+ [Prefixes]
16
+ FIELD=字段"{assignee}"
17
+ OBJECT=对象"{assignee}"
18
+ ARRAY=列表"{assignee}"的索引{index}处
19
+ ROOT=根级别
20
+
@@ -9,8 +9,8 @@ from .error import print_warning, print_error
9
9
  from .loader import load_types
10
10
  from .blueprint import JsonBlueprint
11
11
  from .declaration import create_declaration
12
- from .field import create_field
13
- from .array import make_array
12
+ from .field import create_field, JsonField
13
+ from .array import make_array, is_array
14
14
 
15
15
  reserved = (
16
16
  'root',
@@ -20,7 +20,10 @@ reserved = (
20
20
  'optional',
21
21
  'nullable',
22
22
  'extends',
23
- 'include'
23
+ 'include',
24
+ 'wraps',
25
+ 'union',
26
+ 'on'
24
27
  )
25
28
 
26
29
  literals = (
@@ -32,7 +35,9 @@ literals = (
32
35
  ']',
33
36
  ',',
34
37
  '=',
35
- ':'
38
+ ':',
39
+ '<',
40
+ '>'
36
41
  )
37
42
 
38
43
  # List of tokens
@@ -44,12 +49,17 @@ types = (
44
49
  'FLOAT_CONST',
45
50
  'INTEGER_AMOUNT',
46
51
  'IDENTIFIER',
52
+ 'ARROW',
47
53
  )
48
54
 
49
55
  tokens = list(types) + [token.upper() for token in reserved]
50
56
 
51
57
  #------------------------------------------------------------
52
58
 
59
+ def t_ARROW(t):
60
+ r'=>'
61
+ return t
62
+
53
63
  def t_STRING(t):
54
64
  r'"[^"]*"'
55
65
  t.value = t.value[1:-1]
@@ -117,7 +127,8 @@ def typeExists(typeName, excluded=None):
117
127
  lookups = (
118
128
  currentBlueprint._find_object_decl,
119
129
  currentBlueprint._find_element_decl,
120
- currentBlueprint._find_enum_decl
130
+ currentBlueprint._find_enum_decl,
131
+ currentBlueprint._find_union_decl,
121
132
  )
122
133
 
123
134
  for method in lookups:
@@ -134,6 +145,8 @@ def getNextAdhoc():
134
145
  adhoc_counter += 1
135
146
  return adhoc_counter
136
147
 
148
+ currentTypeParam = None
149
+
137
150
  #---------------- general structure -----------------------------
138
151
 
139
152
  def p_schema(p):
@@ -149,6 +162,7 @@ def p_construction(p):
149
162
  | object
150
163
  | root
151
164
  | include
165
+ | union
152
166
  '''
153
167
 
154
168
  #---------------- constructs -----------------------------
@@ -191,12 +205,39 @@ def p_root(p):
191
205
  currentBlueprint.root = p[2]
192
206
 
193
207
 
208
+ def p_generic_header(p):
209
+ '''
210
+ generic_header : OBJECT IDENTIFIER WRAPS IDENTIFIER
211
+ '''
212
+
213
+ global currentTypeParam
214
+ currentTypeParam = p[4]
215
+ p[0] = p[2]
216
+
217
+
194
218
  def p_object(p):
195
219
  '''
196
- object : OBJECT IDENTIFIER EXTENDS IDENTIFIER object_declaration
220
+ object : generic_header object_declaration
221
+ | OBJECT IDENTIFIER EXTENDS IDENTIFIER object_declaration
197
222
  | OBJECT IDENTIFIER object_declaration
198
223
  '''
199
224
 
225
+ global currentTypeParam
226
+
227
+ if len(p) == 3:
228
+ templateName = p[1]
229
+ paramName = currentTypeParam
230
+ currentTypeParam = None
231
+
232
+ if typeExists(templateName) or currentBlueprint._find_template_decl(templateName) is not None:
233
+ raise SchemaViolation(f"Duplicated type '{templateName}'")
234
+
235
+ currentBlueprint.templates[templateName] = {
236
+ 'param': paramName,
237
+ 'fields': p[2]
238
+ }
239
+ return
240
+
200
241
  objectName = p[2]
201
242
  if typeExists(objectName):
202
243
  msg = f"Duplicated type '{objectName}'"
@@ -229,13 +270,30 @@ def p_object(p):
229
270
  def p_object_specs(p):
230
271
  '''
231
272
  object_declaration : '{' attributes '}'
273
+ | EXTENDS IDENTIFIER '{' attributes '}'
232
274
  '''
233
275
 
234
- decls = p[2]
276
+ if len(p) == 4:
277
+ decls = p[2]
278
+ baseFields = {}
279
+
280
+ else:
281
+ baseObject = p[2]
282
+ baseFields = currentBlueprint._find_object_decl(baseObject)
283
+ if baseFields is None:
284
+ raise SchemaViolation(f"Object '{baseObject}' is not defined")
285
+ decls = p[4]
286
+
235
287
  newObject = dict()
236
288
  for decl in decls:
237
- newObject[decl[0]] = decl[1]
289
+ fieldName = decl[0]
290
+ if fieldName in baseFields:
291
+ raise SchemaViolation(
292
+ f"Field '{fieldName}' already defined in '{baseObject}'"
293
+ )
294
+ newObject[fieldName] = decl[1]
238
295
 
296
+ newObject.update(baseFields)
239
297
  p[0] = newObject
240
298
 
241
299
 
@@ -296,10 +354,12 @@ def p_type(p):
296
354
  def p_attributes(p):
297
355
  '''
298
356
  attributes : attributes ',' attribute
357
+ | attributes ','
299
358
  | attribute
300
359
  '''
301
360
 
302
361
  if len(p) == 4: p[1].append(p[3])
362
+ if len(p) == 3: p[0] = p[1]; return
303
363
  if len(p) == 2: p[1] = [p[1]]
304
364
  p[0] = p[1]
305
365
 
@@ -358,11 +418,66 @@ def p_single_declaration(p):
358
418
  p[0] = p[1]
359
419
 
360
420
 
421
+ def instantiateTemplate(templateName, argType):
422
+ template = currentBlueprint._find_template_decl(templateName)
423
+ if template is None:
424
+ raise SchemaViolation(f"Template '{templateName}' is not defined")
425
+
426
+ if not typeExists(argType):
427
+ raise SchemaViolation(f"Type not declared: '{argType}'")
428
+
429
+ concreteName = f'{templateName}__{argType}'
430
+ if currentBlueprint._find_object_decl(concreteName) is not None:
431
+ return concreteName
432
+
433
+ if currentBlueprint._find_object_decl(argType) is not None:
434
+ argKind = FieldType.OBJECT
435
+ elif currentBlueprint._find_enum_decl(argType) is not None:
436
+ argKind = FieldType.ENUM
437
+ else:
438
+ argKind = FieldType.SIMPLE
439
+
440
+ paramName = template['param']
441
+ concreteFields = dict()
442
+
443
+ for fieldName, field in template['fields'].items():
444
+ if field.fieldType != paramName:
445
+ concreteFields[fieldName] = field
446
+ continue
447
+
448
+ if is_array(field):
449
+ newBase = create_field(argKind, argType)
450
+ newBase.nullable = field.nullable
451
+ newField = make_array(newBase)
452
+ newField.minLength = field.minLength
453
+ newField.maxLength = field.maxLength
454
+ newField.nullableArray = field.nullableArray
455
+ else:
456
+ newField = create_field(argKind, argType)
457
+ newField.nullable = field.nullable
458
+ newField.optional = field.optional
459
+
460
+ concreteFields[fieldName] = newField
461
+
462
+ currentBlueprint.objects[concreteName] = concreteFields
463
+ return concreteName
464
+
465
+
466
+ def p_template_declaration(p):
467
+ '''
468
+ template_declaration : IDENTIFIER '<' IDENTIFIER '>'
469
+ '''
470
+
471
+ concreteName = instantiateTemplate(p[1], p[3])
472
+ p[0] = create_field(FieldType.OBJECT, concreteName)
473
+
474
+
361
475
  def p_atomic_declaration(p):
362
476
  '''
363
477
  atomic_declaration : object_declaration
364
478
  | enum_declaration
365
479
  | element_declaration
480
+ | template_declaration
366
481
  '''
367
482
 
368
483
  declaration = p[1]
@@ -378,6 +493,10 @@ def p_atomic_declaration(p):
378
493
  kind = FieldType.ENUM
379
494
  fieldId = adhoc_enum
380
495
 
496
+ elif isinstance(declaration, JsonField):
497
+ p[0] = declaration
498
+ return
499
+
381
500
  else:
382
501
  declType = declaration.typeName
383
502
  if currentBlueprint._find_element_decl(declType) is not None:
@@ -395,6 +514,10 @@ def p_atomic_declaration(p):
395
514
  kind = FieldType.ENUM
396
515
  fieldId = declType
397
516
 
517
+ elif currentBlueprint._find_union_decl(declType):
518
+ kind = FieldType.UNION
519
+ fieldId = declType
520
+
398
521
  else:
399
522
  kind = FieldType.OBJECT
400
523
  fieldId = declType
@@ -427,7 +550,7 @@ def p_element_declaration(p):
427
550
  specs = p[3]
428
551
 
429
552
  else:
430
- if not typeExists(typeName):
553
+ if typeName != currentTypeParam and not typeExists(typeName):
431
554
  msg = f"Type not declared: '{typeName}'"
432
555
  raise SchemaViolation(msg)
433
556
 
@@ -466,6 +589,65 @@ def p_specified_value(p):
466
589
 
467
590
  p[0] = p[1]
468
591
 
592
+ #---------------- unions ----------------------------
593
+
594
+ def p_union(p):
595
+ '''
596
+ union : UNION IDENTIFIER ON STRING '{' union_branches '}'
597
+ '''
598
+
599
+ union_name = p[2]
600
+ if typeExists(union_name) or currentBlueprint._find_template_decl(union_name) is not None:
601
+ raise SchemaViolation(f"Duplicated type '{union_name}'")
602
+
603
+ currentBlueprint.unions[union_name] = {
604
+ 'discriminator': p[4],
605
+ 'branches': p[6]
606
+ }
607
+
608
+
609
+ def p_union_branches(p):
610
+ '''
611
+ union_branches : union_branches ',' union_branch
612
+ | union_branches ','
613
+ | union_branch
614
+ '''
615
+
616
+ if len(p) == 4:
617
+ p[1].update(p[3])
618
+ p[0] = p[1]
619
+ elif len(p) == 3:
620
+ p[0] = p[1]
621
+ else:
622
+ p[0] = p[1]
623
+
624
+
625
+ def p_union_branch(p):
626
+ '''
627
+ union_branch : STRING ARROW branch_declaration
628
+ '''
629
+
630
+ p[0] = {p[1]: p[3]}
631
+
632
+
633
+ def p_branch_declaration(p):
634
+ '''
635
+ branch_declaration : object_declaration
636
+ | IDENTIFIER
637
+ '''
638
+
639
+ if isinstance(p[1], dict):
640
+ adhoc_name = '_union_branch_' + str(getNextAdhoc()) + '_'
641
+ currentBlueprint.objects[adhoc_name] = p[1]
642
+ p[0] = adhoc_name
643
+
644
+ else:
645
+ type_name = p[1]
646
+ if currentBlueprint._find_object_decl(type_name) is None:
647
+ raise SchemaViolation(f"Object '{type_name}' is not defined")
648
+ p[0] = type_name
649
+
650
+
469
651
  #---------------- enums -----------------------------
470
652
 
471
653
  def p_enum(p):
@@ -492,6 +674,7 @@ def p_enum_declaration(p):
492
674
  def p_constants(p):
493
675
  '''
494
676
  constants : constants ',' IDENTIFIER
677
+ | constants ','
495
678
  | IDENTIFIER
496
679
  '''
497
680
 
@@ -499,6 +682,9 @@ def p_constants(p):
499
682
  p[1].append(p[3])
500
683
  p[0] = p[1]
501
684
 
685
+ elif len(p) == 3:
686
+ p[0] = p[1]
687
+
502
688
  else:
503
689
  p[0] = [p[1]]
504
690
 
@@ -536,7 +722,7 @@ def _load(contents, contentPath, contentName, typeDirs):
536
722
  parser = plyYacc.yacc()
537
723
 
538
724
  ownPath = os.path.dirname(os.path.realpath(__file__))
539
- primitivesPath = os.path.join(ownPath, "types")
725
+ primitivesPath = os.path.join(ownPath, "basetypes")
540
726
  loaded, notLoaded = load_types(primitivesPath)
541
727
 
542
728
  primitive_types = dict()
@@ -589,9 +775,9 @@ def load_file(filepath, **kwargs):
589
775
 
590
776
  '''Loads a :class:`JsonBlueprint` from a file.
591
777
 
592
- On success, the returned instance is associated with the
593
- absolute path of the argument file, and subsequent calls of this
594
- function to the same file will be presented with the same instance,
778
+ On success, the returned instance is associated with the
779
+ absolute path of the argument file, and subsequent calls to this
780
+ function with the same file path will return the same cached instance,
595
781
  unless :func:`invalidate_cache` is invoked.
596
782
 
597
783
  Args:
@@ -651,7 +837,7 @@ def invalidate_cache():
651
837
  '''Clears associations between files and existing JsonBlueprint instances.
652
838
 
653
839
  This forces new :func:`load_file` invocations to effectively parse the files instead
654
- of returning a cached result (Useful when your schema file changed and needs
840
+ of returning a cached result (useful when the schema file has changed and needs
655
841
  to be refreshed).
656
842
 
657
843
  '''
@@ -17,6 +17,7 @@ class FieldType:
17
17
  SIMPLE = 0
18
18
  ENUM = 1
19
19
  OBJECT = 2
20
+ UNION = 3
20
21
 
21
22
 
22
23
  class unquoted_str(str):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: jsonbp
3
- Version: 1.2.2
3
+ Version: 1.3.0
4
4
  Summary: Schema based JSON Parser/Serializer
5
5
  Author-email: Gustavo Venturini <gustavo.c.venturini@gmail.com>
6
6
  License-Expression: BSD-3-Clause
@@ -11,6 +11,7 @@ Classifier: Development Status :: 5 - Production/Stable
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: Operating System :: OS Independent
13
13
  Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
14
15
  Requires-Python: >=3.11
15
16
  Description-Content-Type: text/markdown
16
17
  License-File: LICENSE.txt
@@ -27,7 +28,7 @@ to and from Python based on schemas. While [json-schema][json_schema] and its
27
28
  implementations offer a more mature and widely used technique, a different
28
29
  approach was desired, which led to the development of this library.
29
30
 
30
- jsonbp's design main goals were:
31
+ jsonbp's main design goals were:
31
32
  - schema reuse through import / type system
32
33
  - custom user definable primitive types
33
34
  - built in numeric fixed precision type which deserializes into Python's Decimal
@@ -16,21 +16,25 @@ jsonbp.egg-info/PKG-INFO
16
16
  jsonbp.egg-info/SOURCES.txt
17
17
  jsonbp.egg-info/dependency_links.txt
18
18
  jsonbp.egg-info/top_level.txt
19
+ jsonbp/basetypes/__init__.py
20
+ jsonbp/basetypes/bool.py
21
+ jsonbp/basetypes/date.py
22
+ jsonbp/basetypes/decimal.py
23
+ jsonbp/basetypes/float.py
24
+ jsonbp/basetypes/instant.py
25
+ jsonbp/basetypes/integer.py
26
+ jsonbp/basetypes/limits.py
27
+ jsonbp/basetypes/string.py
19
28
  jsonbp/localization/__init__.py
29
+ jsonbp/localization/de_DE.ini
20
30
  jsonbp/localization/en_US.ini
31
+ jsonbp/localization/fr_FR.ini
21
32
  jsonbp/localization/pt_BR.ini
33
+ jsonbp/localization/ru_RU.ini
34
+ jsonbp/localization/zh_CN.ini
22
35
  jsonbp/ply/__init__.py
23
36
  jsonbp/ply/lex.py
24
37
  jsonbp/ply/yacc.py
25
- jsonbp/types/__init__.py
26
- jsonbp/types/bool.py
27
- jsonbp/types/date.py
28
- jsonbp/types/decimal.py
29
- jsonbp/types/float.py
30
- jsonbp/types/instant.py
31
- jsonbp/types/integer.py
32
- jsonbp/types/limits.py
33
- jsonbp/types/string.py
34
38
  tests/test_caching.py
35
39
  tests/test_choose_root.py
36
40
  tests/test_deserialization.py
@@ -4,11 +4,16 @@ requires = ["setuptools"]
4
4
  build-backend = "setuptools.build_meta"
5
5
 
6
6
  [tool.setuptools]
7
- packages = ["jsonbp", "jsonbp.ply", "jsonbp.types", "jsonbp.localization"]
7
+ packages = [
8
+ "jsonbp",
9
+ "jsonbp.basetypes",
10
+ "jsonbp.localization",
11
+ "jsonbp.ply"
12
+ ]
8
13
 
9
14
  [project]
10
15
  name = "jsonbp"
11
- version = "1.2.2"
16
+ version = "1.3.0"
12
17
  description = "Schema based JSON Parser/Serializer"
13
18
  authors = [
14
19
  { name = "Gustavo Venturini", email = "gustavo.c.venturini@gmail.com" }
@@ -22,6 +27,7 @@ classifiers = [
22
27
  "Intended Audience :: Developers",
23
28
  "Operating System :: OS Independent",
24
29
  "Programming Language :: Python :: 3 :: Only",
30
+ "Topic :: Software Development :: Libraries :: Python Modules"
25
31
  ]
26
32
 
27
33
  [project.urls]
@@ -4,7 +4,7 @@ import os.path
4
4
  import sys
5
5
 
6
6
  from decimal import Decimal
7
- from datetime import datetime, timedelta, timezone
7
+ from datetime import date, datetime, timedelta, timezone
8
8
  import pytest
9
9
 
10
10
  verificationDir = 'deserialization'
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes