jsonschema.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. from dataclasses import dataclass, field
  2. from enum import Enum
  3. from typing import Any
  4. from django import forms
  5. from django.contrib.postgres.forms import SimpleArrayField
  6. from django.core.exceptions import ValidationError
  7. from django.core.validators import RegexValidator
  8. from django.utils.translation import gettext_lazy as _
  9. from jsonschema.exceptions import SchemaError
  10. from jsonschema.validators import validator_for
  11. from utilities.string import title
  12. from utilities.validators import MultipleOfValidator
  13. __all__ = (
  14. 'JSONSchemaProperty',
  15. 'PropertyTypeEnum',
  16. 'StringFormatEnum',
  17. 'validate_schema',
  18. )
  19. class PropertyTypeEnum(Enum):
  20. STRING = 'string'
  21. INTEGER = 'integer'
  22. NUMBER = 'number'
  23. BOOLEAN = 'boolean'
  24. ARRAY = 'array'
  25. OBJECT = 'object'
  26. class StringFormatEnum(Enum):
  27. EMAIL = 'email'
  28. URI = 'uri'
  29. IRI = 'iri'
  30. UUID = 'uuid'
  31. DATE = 'date'
  32. TIME = 'time'
  33. DATETIME = 'datetime'
  34. FORM_FIELDS = {
  35. PropertyTypeEnum.STRING.value: forms.CharField,
  36. PropertyTypeEnum.INTEGER.value: forms.IntegerField,
  37. PropertyTypeEnum.NUMBER.value: forms.FloatField,
  38. PropertyTypeEnum.BOOLEAN.value: forms.BooleanField,
  39. PropertyTypeEnum.ARRAY.value: SimpleArrayField,
  40. PropertyTypeEnum.OBJECT.value: forms.JSONField,
  41. }
  42. STRING_FORM_FIELDS = {
  43. StringFormatEnum.EMAIL.value: forms.EmailField,
  44. StringFormatEnum.URI.value: forms.URLField,
  45. StringFormatEnum.IRI.value: forms.URLField,
  46. StringFormatEnum.UUID.value: forms.UUIDField,
  47. StringFormatEnum.DATE.value: forms.DateField,
  48. StringFormatEnum.TIME.value: forms.TimeField,
  49. StringFormatEnum.DATETIME.value: forms.DateTimeField,
  50. }
  51. @dataclass
  52. class JSONSchemaProperty:
  53. type: PropertyTypeEnum = PropertyTypeEnum.STRING.value
  54. title: str | None = None
  55. description: str | None = None
  56. default: Any = None
  57. enum: list | None = None
  58. # Strings
  59. minLength: int | None = None
  60. maxLength: int | None = None
  61. pattern: str | None = None # Regex
  62. format: StringFormatEnum | None = None
  63. # Numbers
  64. minimum: int | float | None = None
  65. maximum: int | float | None = None
  66. multipleOf: int | float | None = None
  67. # Arrays
  68. items: dict | None = field(default_factory=dict)
  69. def to_form_field(self, name, required=False):
  70. """
  71. Instantiate and return a Django form field suitable for editing the property's value.
  72. """
  73. field_kwargs = {
  74. 'label': self.title or title(name),
  75. 'help_text': self.description,
  76. 'required': required,
  77. 'initial': self.default,
  78. }
  79. # Choices
  80. if self.enum:
  81. choices = [(v, v) for v in self.enum]
  82. if not required:
  83. choices = [(None, ''), *choices]
  84. field_kwargs['choices'] = choices
  85. # Arrays
  86. if self.type == PropertyTypeEnum.ARRAY.value:
  87. items_type = self.items.get('type', PropertyTypeEnum.STRING.value)
  88. field_kwargs['base_field'] = FORM_FIELDS[items_type]()
  89. # String validation
  90. if self.type == PropertyTypeEnum.STRING.value:
  91. if self.minLength is not None:
  92. field_kwargs['min_length'] = self.minLength
  93. if self.maxLength is not None:
  94. field_kwargs['max_length'] = self.maxLength
  95. if self.pattern is not None:
  96. field_kwargs['validators'] = [
  97. RegexValidator(regex=self.pattern)
  98. ]
  99. # Integer/number validation
  100. elif self.type in (PropertyTypeEnum.INTEGER.value, PropertyTypeEnum.NUMBER.value):
  101. field_kwargs['widget'] = forms.NumberInput(attrs={'step': 'any'})
  102. if self.minimum:
  103. field_kwargs['min_value'] = self.minimum
  104. if self.maximum:
  105. field_kwargs['max_value'] = self.maximum
  106. if self.multipleOf:
  107. field_kwargs['validators'] = [
  108. MultipleOfValidator(multiple=self.multipleOf)
  109. ]
  110. return self.field_class(**field_kwargs)
  111. @property
  112. def field_class(self):
  113. """
  114. Resolve the property's type (and string format, if specified) to the appropriate field class.
  115. """
  116. if self.enum:
  117. if self.type == PropertyTypeEnum.ARRAY.value:
  118. return forms.MultipleChoiceField
  119. return forms.ChoiceField
  120. if self.type == PropertyTypeEnum.STRING.value and self.format is not None:
  121. try:
  122. return STRING_FORM_FIELDS[self.format]
  123. except KeyError:
  124. raise ValueError(f"Unsupported string format type: {self.format}")
  125. try:
  126. return FORM_FIELDS[self.type]
  127. except KeyError:
  128. raise ValueError(f"Unknown property type: {self.type}")
  129. def validate_schema(schema):
  130. """
  131. Check that a minimum JSON schema definition is defined.
  132. """
  133. # Pass on empty values
  134. if schema in (None, ''):
  135. return
  136. # Provide some basic sanity checking (not provided by jsonschema)
  137. if type(schema) is not dict:
  138. raise ValidationError(_("Invalid JSON schema definition"))
  139. if not schema.get('properties'):
  140. raise ValidationError(_("JSON schema must define properties"))
  141. try:
  142. ValidatorClass = validator_for(schema)
  143. ValidatorClass.check_schema(schema)
  144. except SchemaError as e:
  145. raise ValidationError(_("Invalid JSON schema definition: {error}").format(error=e))