Schema Validation class factory.

    Args:
        schema_name(str): The namespace of the schema.
        schema_nodes(dict): The attr_names / SchemaNodes mapping of schema.

    Returns:
        A Schema class.

    Raises:
        SchemaError, for bad attribute setting initialization.

    Examples:

        >>> from schema_factory import FloatNode, StringNode, SchemaNode
        >>>
        >>> PointSchema = schema_factory(
        ...     schema_name='point',
        ...     lat=FloatNode(),
        ...     lng=FloatNode(),
        ... )
        ...
        >>> point = PointSchema(lat=34, lng=29.01)
        >>> print(point.to_dict)
        OrderedDict([('lat', 34.0), ('lng', 29.01)])
        >>> point2 = PointSchema(lat='34', lng='0')
        >>> print(point2.to_dict)
        OrderedDict([('lat', 34.0), ('lng', 0.0)])
        >>> RegionSchema = schema_factory(
        ...     schema_name='Region',
        ...     name=StringNode(),
        ...     country_code=StringNode( required=True, validators=[lambda x: len(x) == 2]),
        ...     location=SchemaNode(PointSchema, required=False, default=None),
        ...     keywords=StringNode(array=True, required=False, default=[])
        ... )
        ...
        >>> region = RegionSchema(name='Athens', country_code='gr', location={'lat': 32.7647, 'lng': 27.03})
        >>> print(region)
        <RegionSchema instance, attributes:['country_code', 'keywords', 'location', 'name']>
        >>> region.keywords
        []
        >>> region2 = RegionSchema(name='Athens')
        Traceback (most recent call last):
            ...
        schema_factory.errors.SchemaError: Missing Required Attributes: {'country_code'}
        >>> region3 = RegionSchema(name='Athens', country_code='gr', location={'lat': 32.7647, 'lng': 27.03},
        ...     foo='bar')
        Traceback (most recent call last):
            ...
        schema_factory.errors.SchemaError: Invalid Attributes RegionSchema for {'foo'}.
        >>> region4 = RegionSchema(name='Athens', country_code='gr', keywords=['Acropolis', 'Mousaka', 434132])

def schema_factory(schema_name, **schema_nodes):
    """Schema Validation class factory.

    Args:
        schema_name(str): The namespace of the schema.
        schema_nodes(dict): The attr_names / SchemaNodes mapping of schema.

    Returns:
        A Schema class.

    Raises:
        SchemaError, for bad attribute setting initialization.

    Examples:

        >>> from schema_factory import FloatNode, StringNode, SchemaNode
        >>>
        >>> PointSchema = schema_factory(
        ...     schema_name='point',
        ...     lat=FloatNode(),
        ...     lng=FloatNode(),
        ... )
        ...
        >>> point = PointSchema(lat=34, lng=29.01)
        >>> print(point.to_dict)
        OrderedDict([('lat', 34.0), ('lng', 29.01)])
        >>> point2 = PointSchema(lat='34', lng='0')
        >>> print(point2.to_dict)
        OrderedDict([('lat', 34.0), ('lng', 0.0)])
        >>> RegionSchema = schema_factory(
        ...     schema_name='Region',
        ...     name=StringNode(),
        ...     country_code=StringNode( required=True, validators=[lambda x: len(x) == 2]),
        ...     location=SchemaNode(PointSchema, required=False, default=None),
        ...     keywords=StringNode(array=True, required=False, default=[])
        ... )
        ...
        >>> region = RegionSchema(name='Athens', country_code='gr', location={'lat': 32.7647, 'lng': 27.03})
        >>> print(region)
        <RegionSchema instance, attributes:['country_code', 'keywords', 'location', 'name']>
        >>> region.keywords
        []
        >>> region2 = RegionSchema(name='Athens')
        Traceback (most recent call last):
            ...
        schema_factory.errors.SchemaError: Missing Required Attributes: {'country_code'}
        >>> region3 = RegionSchema(name='Athens', country_code='gr', location={'lat': 32.7647, 'lng': 27.03},
        ...     foo='bar')
        Traceback (most recent call last):
            ...
        schema_factory.errors.SchemaError: Invalid Attributes RegionSchema for {'foo'}.
        >>> region4 = RegionSchema(name='Athens', country_code='gr', keywords=['Acropolis', 'Mousaka', 434132])
    """

    schema_dict = dict()
    schema_dict.update(schema_nodes)

    def cls_repr(self):  # pragma: no cover
        return "<{} instance at: 0x{:x}>".format(self.__class__, id(self))

    def cls_str(self):   # pragma: no cover
        return "<{} instance, attributes:{}>".format(
            self.__class__.__name__,
            self.schema_nodes
        )

    def cls_init(self, **kwargs):

        kwargs_set = set(kwargs)

        if not self.required.issubset(kwargs_set):
            raise SchemaError('Missing Required Attributes: {}'.format(
                self.required.difference(kwargs_set)
            ))

        if not set(kwargs).issubset(set(self.schema_nodes)):
            raise SchemaError('Invalid Attributes {} for {}.'.format(
                self.__class__.__name__,
                set(kwargs).difference(set(self.schema_nodes))
            ))

        for attr_name in kwargs:
            setattr(self, attr_name, kwargs[attr_name])

    def to_dict(self):
        return OrderedDict([(k, getattr(self, k)) for k in self.schema_nodes])

    schema_dict['to_dict'] = property(to_dict)
    schema_dict['__init__'] = cls_init
    schema_dict['__repr__'] = cls_repr
    schema_dict['__str__'] = cls_str

    return SchemaType('{}Schema'.format(schema_name.title()), (), schema_dict)