| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- from dataclasses import dataclass, field
- from enum import Enum
- from typing import Any
- from django import forms
- from django.contrib.postgres.forms import SimpleArrayField
- from django.core.exceptions import ValidationError
- from django.core.validators import RegexValidator
- from django.utils.translation import gettext_lazy as _
- from jsonschema.exceptions import SchemaError
- from jsonschema.validators import validator_for
- from utilities.string import title
- from utilities.validators import MultipleOfValidator
- __all__ = (
- 'JSONSchemaProperty',
- 'PropertyTypeEnum',
- 'StringFormatEnum',
- 'validate_schema',
- )
- class PropertyTypeEnum(Enum):
- STRING = 'string'
- INTEGER = 'integer'
- NUMBER = 'number'
- BOOLEAN = 'boolean'
- ARRAY = 'array'
- OBJECT = 'object'
- class StringFormatEnum(Enum):
- EMAIL = 'email'
- URI = 'uri'
- IRI = 'iri'
- UUID = 'uuid'
- DATE = 'date'
- TIME = 'time'
- DATETIME = 'datetime'
- FORM_FIELDS = {
- PropertyTypeEnum.STRING.value: forms.CharField,
- PropertyTypeEnum.INTEGER.value: forms.IntegerField,
- PropertyTypeEnum.NUMBER.value: forms.FloatField,
- PropertyTypeEnum.BOOLEAN.value: forms.BooleanField,
- PropertyTypeEnum.ARRAY.value: SimpleArrayField,
- PropertyTypeEnum.OBJECT.value: forms.JSONField,
- }
- STRING_FORM_FIELDS = {
- StringFormatEnum.EMAIL.value: forms.EmailField,
- StringFormatEnum.URI.value: forms.URLField,
- StringFormatEnum.IRI.value: forms.URLField,
- StringFormatEnum.UUID.value: forms.UUIDField,
- StringFormatEnum.DATE.value: forms.DateField,
- StringFormatEnum.TIME.value: forms.TimeField,
- StringFormatEnum.DATETIME.value: forms.DateTimeField,
- }
- @dataclass
- class JSONSchemaProperty:
- type: PropertyTypeEnum = PropertyTypeEnum.STRING.value
- title: str | None = None
- description: str | None = None
- default: Any = None
- enum: list | None = None
- # Strings
- minLength: int | None = None
- maxLength: int | None = None
- pattern: str | None = None # Regex
- format: StringFormatEnum | None = None
- # Numbers
- minimum: int | float | None = None
- maximum: int | float | None = None
- multipleOf: int | float | None = None
- # Arrays
- items: dict | None = field(default_factory=dict)
- def to_form_field(self, name, required=False):
- """
- Instantiate and return a Django form field suitable for editing the property's value.
- """
- field_kwargs = {
- 'label': self.title or title(name),
- 'help_text': self.description,
- 'required': required,
- 'initial': self.default,
- }
- # Choices
- if self.enum:
- choices = [(v, v) for v in self.enum]
- if not required:
- choices = [(None, ''), *choices]
- field_kwargs['choices'] = choices
- # Arrays
- if self.type == PropertyTypeEnum.ARRAY.value:
- items_type = self.items.get('type', PropertyTypeEnum.STRING.value)
- field_kwargs['base_field'] = FORM_FIELDS[items_type]()
- # String validation
- if self.type == PropertyTypeEnum.STRING.value:
- if self.minLength is not None:
- field_kwargs['min_length'] = self.minLength
- if self.maxLength is not None:
- field_kwargs['max_length'] = self.maxLength
- if self.pattern is not None:
- field_kwargs['validators'] = [
- RegexValidator(regex=self.pattern)
- ]
- # Integer/number validation
- elif self.type in (PropertyTypeEnum.INTEGER.value, PropertyTypeEnum.NUMBER.value):
- field_kwargs['widget'] = forms.NumberInput(attrs={'step': 'any'})
- if self.minimum:
- field_kwargs['min_value'] = self.minimum
- if self.maximum:
- field_kwargs['max_value'] = self.maximum
- if self.multipleOf:
- field_kwargs['validators'] = [
- MultipleOfValidator(multiple=self.multipleOf)
- ]
- return self.field_class(**field_kwargs)
- @property
- def field_class(self):
- """
- Resolve the property's type (and string format, if specified) to the appropriate field class.
- """
- if self.enum:
- if self.type == PropertyTypeEnum.ARRAY.value:
- return forms.MultipleChoiceField
- return forms.ChoiceField
- if self.type == PropertyTypeEnum.STRING.value and self.format is not None:
- try:
- return STRING_FORM_FIELDS[self.format]
- except KeyError:
- raise ValueError(f"Unsupported string format type: {self.format}")
- try:
- return FORM_FIELDS[self.type]
- except KeyError:
- raise ValueError(f"Unknown property type: {self.type}")
- def validate_schema(schema):
- """
- Check that a minimum JSON schema definition is defined.
- """
- # Pass on empty values
- if schema in (None, ''):
- return
- # Provide some basic sanity checking (not provided by jsonschema)
- if type(schema) is not dict:
- raise ValidationError(_("Invalid JSON schema definition"))
- if not schema.get('properties'):
- raise ValidationError(_("JSON schema must define properties"))
- try:
- ValidatorClass = validator_for(schema)
- ValidatorClass.check_schema(schema)
- except SchemaError as e:
- raise ValidationError(_("Invalid JSON schema definition: {error}").format(error=e))
|