shale 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +270 -5
  4. data/exe/shaleb +75 -0
  5. data/lib/shale/adapter/json.rb +7 -2
  6. data/lib/shale/adapter/nokogiri.rb +48 -12
  7. data/lib/shale/adapter/ox.rb +28 -4
  8. data/lib/shale/adapter/rexml.rb +56 -13
  9. data/lib/shale/attribute.rb +1 -1
  10. data/lib/shale/error.rb +6 -0
  11. data/lib/shale/mapper.rb +11 -11
  12. data/lib/shale/mapping/descriptor/dict.rb +57 -0
  13. data/lib/shale/mapping/descriptor/xml.rb +43 -0
  14. data/lib/shale/mapping/descriptor/xml_namespace.rb +37 -0
  15. data/lib/shale/mapping/{key_value.rb → dict.rb} +8 -6
  16. data/lib/shale/mapping/validator.rb +51 -0
  17. data/lib/shale/mapping/xml.rb +86 -15
  18. data/lib/shale/schema/json/base.rb +41 -0
  19. data/lib/shale/schema/json/boolean.rb +23 -0
  20. data/lib/shale/schema/json/collection.rb +39 -0
  21. data/lib/shale/schema/json/date.rb +23 -0
  22. data/lib/shale/schema/json/float.rb +23 -0
  23. data/lib/shale/schema/json/integer.rb +23 -0
  24. data/lib/shale/schema/json/object.rb +37 -0
  25. data/lib/shale/schema/json/ref.rb +28 -0
  26. data/lib/shale/schema/json/schema.rb +57 -0
  27. data/lib/shale/schema/json/string.rb +23 -0
  28. data/lib/shale/schema/json/time.rb +23 -0
  29. data/lib/shale/schema/json.rb +165 -0
  30. data/lib/shale/schema/xml/attribute.rb +41 -0
  31. data/lib/shale/schema/xml/complex_type.rb +67 -0
  32. data/lib/shale/schema/xml/element.rb +55 -0
  33. data/lib/shale/schema/xml/import.rb +46 -0
  34. data/lib/shale/schema/xml/ref_attribute.rb +37 -0
  35. data/lib/shale/schema/xml/ref_element.rb +39 -0
  36. data/lib/shale/schema/xml/schema.rb +121 -0
  37. data/lib/shale/schema/xml/typed_attribute.rb +46 -0
  38. data/lib/shale/schema/xml/typed_element.rb +46 -0
  39. data/lib/shale/schema/xml.rb +316 -0
  40. data/lib/shale/schema.rb +47 -0
  41. data/lib/shale/type/boolean.rb +2 -2
  42. data/lib/shale/type/composite.rb +66 -54
  43. data/lib/shale/type/date.rb +35 -2
  44. data/lib/shale/type/float.rb +2 -2
  45. data/lib/shale/type/integer.rb +2 -2
  46. data/lib/shale/type/string.rb +2 -2
  47. data/lib/shale/type/time.rb +35 -2
  48. data/lib/shale/type/{base.rb → value.rb} +18 -7
  49. data/lib/shale/utils.rb +18 -2
  50. data/lib/shale/version.rb +1 -1
  51. data/lib/shale.rb +10 -10
  52. data/shale.gemspec +6 -2
  53. metadata +41 -13
  54. data/lib/shale/mapping/base.rb +0 -32
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shale
4
+ module Schema
5
+ class JSON
6
+ # Class representing array type in JSON Schema
7
+ #
8
+ # @api private
9
+ class Collection
10
+ # Initialize Collection object
11
+ #
12
+ # @param [Shale::Schema::JSON::Base] type
13
+ #
14
+ # @api private
15
+ def initialize(type)
16
+ @type = type
17
+ end
18
+
19
+ # Delegate name to wrapped type object
20
+ #
21
+ # @return [String]
22
+ #
23
+ # @api private
24
+ def name
25
+ @type.name
26
+ end
27
+
28
+ # Return JSON Schema fragment as Ruby Hash
29
+ #
30
+ # @return [Hash]
31
+ #
32
+ # @api private
33
+ def as_json
34
+ { 'type' => 'array', 'items' => @type.as_type }
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Shale
6
+ module Schema
7
+ class JSON
8
+ # Class representing JSON Schema date type
9
+ #
10
+ # @api private
11
+ class Date < Base
12
+ # Return JSON Schema fragment as Ruby Hash
13
+ #
14
+ # @return [Hash]
15
+ #
16
+ # @api private
17
+ def as_type
18
+ { 'type' => 'string', 'format' => 'date' }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Shale
6
+ module Schema
7
+ class JSON
8
+ # Class representing JSON Schema float type
9
+ #
10
+ # @api private
11
+ class Float < Base
12
+ # Return JSON Schema fragment as Ruby Hash
13
+ #
14
+ # @return [Hash]
15
+ #
16
+ # @api private
17
+ def as_type
18
+ { 'type' => 'number' }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Shale
6
+ module Schema
7
+ class JSON
8
+ # Class representing JSON Schema integer type
9
+ #
10
+ # @api private
11
+ class Integer < Base
12
+ # Return JSON Schema fragment as Ruby Hash
13
+ #
14
+ # @return [Hash]
15
+ #
16
+ # @api private
17
+ def as_type
18
+ { 'type' => 'integer' }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Shale
6
+ module Schema
7
+ class JSON
8
+ # Class representing JSON Schema object type
9
+ #
10
+ # @api private
11
+ class Object < Base
12
+ # Initialize object
13
+ #
14
+ # @param [String] name
15
+ # @param [Array<Shale::Schema::JSON::Base, Shale::Schema::JSON::Collection>] properties
16
+ #
17
+ # @api private
18
+ def initialize(name, properties)
19
+ super(name)
20
+ @properties = properties
21
+ end
22
+
23
+ # Return JSON Schema fragment as Ruby Hash
24
+ #
25
+ # @return [Hash]
26
+ #
27
+ # @api private
28
+ def as_type
29
+ {
30
+ 'type' => 'object',
31
+ 'properties' => @properties.to_h { |el| [el.name, el.as_json] },
32
+ }
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Shale
6
+ module Schema
7
+ class JSON
8
+ # Class representing JSON Schema reference
9
+ #
10
+ # @api private
11
+ class Ref < Base
12
+ def initialize(name, type)
13
+ super(name)
14
+ @type = type.gsub('::', '_')
15
+ end
16
+
17
+ # Return JSON Schema fragment as Ruby Hash
18
+ #
19
+ # @return [Hash]
20
+ #
21
+ # @api private
22
+ def as_type
23
+ { '$ref' => "#/$defs/#{@type}" }
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shale
4
+ module Schema
5
+ class JSON
6
+ # Class representing JSON schema
7
+ #
8
+ # @api private
9
+ class Schema
10
+ # JSON Schema dialect (aka version)
11
+ # @api private
12
+ DIALECT = 'https://json-schema.org/draft/2020-12/schema'
13
+
14
+ # Initialize Schema object
15
+ #
16
+ # @param [Array<Shale::Schema::JSON::Base>] types
17
+ # @param [String, nil] id
18
+ # @param [String, nil] description
19
+ #
20
+ # @api private
21
+ def initialize(types, id: nil, description: nil)
22
+ @types = types
23
+ @id = id
24
+ @description = description
25
+ end
26
+
27
+ # Return JSON schema as Ruby Hash
28
+ #
29
+ # @return [Hash]
30
+ #
31
+ # @example
32
+ # Shale::Schema::JSON::Schema.new(types).as_json
33
+ #
34
+ # @api private
35
+ def as_json
36
+ schema = {
37
+ '$schema' => DIALECT,
38
+ 'id' => @id,
39
+ 'description' => @description,
40
+ }
41
+
42
+ unless @types.empty?
43
+ root = @types.first
44
+ root.nullable = false
45
+
46
+ schema['$ref'] = "#/$defs/#{root.name}"
47
+ schema['$defs'] = @types
48
+ .sort { |a, b| a.name <=> b.name }
49
+ .to_h { |e| [e.name, e.as_json] }
50
+ end
51
+
52
+ schema.compact
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Shale
6
+ module Schema
7
+ class JSON
8
+ # Class representing JSON Schema string type
9
+ #
10
+ # @api private
11
+ class String < Base
12
+ # Return JSON Schema fragment as Ruby Hash
13
+ #
14
+ # @return [Hash]
15
+ #
16
+ # @api private
17
+ def as_type
18
+ { 'type' => 'string' }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Shale
6
+ module Schema
7
+ class JSON
8
+ # Class representing JSON Schema time type
9
+ #
10
+ # @api private
11
+ class Time < Base
12
+ # Return JSON Schema fragment as Ruby Hash
13
+ #
14
+ # @return [Hash]
15
+ #
16
+ # @api private
17
+ def as_type
18
+ { 'type' => 'string', 'format' => 'date-time' }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../shale'
4
+ require_relative 'json/schema'
5
+ require_relative 'json/boolean'
6
+ require_relative 'json/collection'
7
+ require_relative 'json/date'
8
+ require_relative 'json/float'
9
+ require_relative 'json/integer'
10
+ require_relative 'json/object'
11
+ require_relative 'json/ref'
12
+ require_relative 'json/string'
13
+ require_relative 'json/time'
14
+
15
+ module Shale
16
+ module Schema
17
+ # Class for handling JSON schema
18
+ #
19
+ # @api public
20
+ class JSON
21
+ @json_types = Hash.new(Shale::Schema::JSON::String)
22
+
23
+ # Register Shale to JSON type mapping
24
+ #
25
+ # @param [Shale::Type::Value] shale_type
26
+ # @param [Shale::Schema::JSON::Base] json_type
27
+ #
28
+ # @example
29
+ # Shale::Schema::JSON.register_json_type(Shale::Type::String, MyCustomJsonType)
30
+ #
31
+ # @api public
32
+ def self.register_json_type(shale_type, json_type)
33
+ @json_types[shale_type] = json_type
34
+ end
35
+
36
+ # Return JSON type for given Shale type
37
+ #
38
+ # @param [Shale::Type::Value] shale_type
39
+ #
40
+ # @return [Shale::Schema::JSON::Base]
41
+ #
42
+ # @example
43
+ # Shale::Schema::JSON.get_json_type(Shale::Type::String)
44
+ # # => Shale::Schema::JSON::String
45
+ #
46
+ # @api private
47
+ def self.get_json_type(shale_type)
48
+ @json_types[shale_type]
49
+ end
50
+
51
+ register_json_type(Shale::Type::Boolean, Shale::Schema::JSON::Boolean)
52
+ register_json_type(Shale::Type::Date, Shale::Schema::JSON::Date)
53
+ register_json_type(Shale::Type::Float, Shale::Schema::JSON::Float)
54
+ register_json_type(Shale::Type::Integer, Shale::Schema::JSON::Integer)
55
+ register_json_type(Shale::Type::Time, Shale::Schema::JSON::Time)
56
+
57
+ # Generate JSON Schema from Shale model and return it as a Ruby Hash
58
+ #
59
+ # @param [Shale::Mapper] klass
60
+ # @param [String, nil] id
61
+ # @param [String, nil] description
62
+ #
63
+ # @raise [NotAShaleMapperError] when attribute is not a Shale model
64
+ #
65
+ # @return [Hash]
66
+ #
67
+ # @example
68
+ # Shale::Schema::JSON.new.as_schema(Person)
69
+ #
70
+ # @api public
71
+ def as_schema(klass, id: nil, description: nil)
72
+ unless mapper_type?(klass)
73
+ raise NotAShaleMapperError, "JSON Shema can't be generated for '#{klass}' type"
74
+ end
75
+
76
+ types = collect_composite_types(klass)
77
+ objects = []
78
+
79
+ types.each do |type|
80
+ properties = []
81
+
82
+ type.json_mapping.keys.values.each do |mapping|
83
+ attribute = type.attributes[mapping.attribute]
84
+ next unless attribute
85
+
86
+ if mapper_type?(attribute.type)
87
+ json_type = Ref.new(mapping.name, attribute.type.name)
88
+ else
89
+ json_klass = self.class.get_json_type(attribute.type)
90
+
91
+ if attribute.default && !attribute.collection?
92
+ value = attribute.type.cast(attribute.default.call)
93
+ default = attribute.type.as_json(value)
94
+ end
95
+
96
+ json_type = json_klass.new(mapping.name, default: default)
97
+ end
98
+
99
+ json_type = Collection.new(json_type) if attribute.collection?
100
+ properties << json_type
101
+ end
102
+
103
+ objects << Object.new(type.name, properties)
104
+ end
105
+
106
+ Schema.new(objects, id: id, description: description).as_json
107
+ end
108
+
109
+ # Generate JSON Schema from Shale model
110
+ #
111
+ # @param [Shale::Mapper] klass
112
+ # @param [String, nil] id
113
+ # @param [String, nil] description
114
+ # @param [true, false] pretty
115
+ #
116
+ # @return [String]
117
+ #
118
+ # @example
119
+ # Shale::Schema::JSON.new.to_schema(Person)
120
+ #
121
+ # @api public
122
+ def to_schema(klass, id: nil, description: nil, pretty: false)
123
+ schema = as_schema(klass, id: id, description: description)
124
+ options = pretty ? :pretty : nil
125
+
126
+ Shale.json_adapter.dump(schema, options)
127
+ end
128
+
129
+ private
130
+
131
+ # Check it type inherits from Shale::Mapper
132
+ #
133
+ # @param [Class] type
134
+ #
135
+ # @return [true, false]
136
+ #
137
+ # @api private
138
+ def mapper_type?(type)
139
+ type < Shale::Mapper
140
+ end
141
+
142
+ # Collect recursively Shale::Mapper types
143
+ #
144
+ # @param [Shale::Mapper] type
145
+ #
146
+ # @return [Array<Shale::Mapper>]
147
+ #
148
+ # @api private
149
+ def collect_composite_types(type)
150
+ types = [type]
151
+
152
+ type.json_mapping.keys.values.each do |mapping|
153
+ attribute = type.attributes[mapping.attribute]
154
+ next unless attribute
155
+
156
+ if mapper_type?(attribute.type) && !types.include?(attribute.type)
157
+ types += collect_composite_types(attribute.type)
158
+ end
159
+ end
160
+
161
+ types.uniq
162
+ end
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shale
4
+ module Schema
5
+ class XML
6
+ # Class representing XML Schema <attribute> element.
7
+ # Serves as a base class for TypedAttribute and RefAttribute
8
+ #
9
+ # @api private
10
+ class Attribute
11
+ # Initialize Attribute object
12
+ #
13
+ # @param [String, nil] default
14
+ #
15
+ # @api private
16
+ def initialize(default)
17
+ @default = default
18
+ end
19
+
20
+ # Append element to the XML document
21
+ #
22
+ # @param [Shale::Adapter::<XML adapter>::Document] doc
23
+ #
24
+ # @return [Shale::Adapter::<XML adapter>::Node]
25
+ #
26
+ # @api private
27
+ def as_xml(doc)
28
+ attribute = doc.create_element('xs:attribute')
29
+
30
+ attributes.each do |name, value|
31
+ doc.add_attribute(attribute, name, value)
32
+ end
33
+
34
+ doc.add_attribute(attribute, 'default', @default) unless @default.nil?
35
+
36
+ attribute
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'attribute'
4
+ require_relative 'element'
5
+
6
+ module Shale
7
+ module Schema
8
+ class XML
9
+ # Class representing XML Schema <complexType> element
10
+ #
11
+ # @api private
12
+ class ComplexType
13
+ # Return name
14
+ #
15
+ # @return [String]
16
+ #
17
+ # @api private
18
+ attr_reader :name
19
+
20
+ # Initialize ComplexType object
21
+ #
22
+ # @param [String] name
23
+ # @param [Array<Shale::Schema::XML::Element, Shale::Schema::XML::Attribute>] children
24
+ # @param [true, false] mixed
25
+ #
26
+ # @api private
27
+ def initialize(name, children = [], mixed: false)
28
+ @name = name
29
+ @children = children
30
+ @mixed = mixed
31
+ end
32
+
33
+ # Append element to the XML document
34
+ #
35
+ # @param [Shale::Adapter::<XML adapter>::Document] doc
36
+ #
37
+ # @return [Shale::Adapter::<XML adapter>::Node]
38
+ #
39
+ # @api private
40
+ def as_xml(doc)
41
+ complex_type = doc.create_element('xs:complexType')
42
+
43
+ doc.add_attribute(complex_type, 'name', @name)
44
+ doc.add_attribute(complex_type, 'mixed', 'true') if @mixed
45
+
46
+ elements = @children.select { |e| e.is_a?(Element) }
47
+ attributes = @children.select { |e| e.is_a?(Attribute) }
48
+
49
+ unless elements.empty?
50
+ sequence = doc.create_element('xs:sequence')
51
+ doc.add_element(complex_type, sequence)
52
+
53
+ elements.each do |element|
54
+ doc.add_element(sequence, element.as_xml(doc))
55
+ end
56
+ end
57
+
58
+ attributes.each do |attribute|
59
+ doc.add_element(complex_type, attribute.as_xml(doc))
60
+ end
61
+
62
+ complex_type
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shale
4
+ module Schema
5
+ class XML
6
+ # Class representing XML Schema <element> element.
7
+ # Serves as a base class for TypedElement and RefElement
8
+ #
9
+ # @api private
10
+ class Element
11
+ # Initialize Element object
12
+ #
13
+ # @param [String, nil] default
14
+ # @param [true, false] collection
15
+ # @param [true, false] required
16
+ #
17
+ # @api private
18
+ def initialize(default, collection, required)
19
+ @default = default
20
+ @collection = collection
21
+ @required = required
22
+ end
23
+
24
+ # Append element to the XML document
25
+ #
26
+ # @param [Shale::Adapter::<XML adapter>::Document] doc
27
+ #
28
+ # @return [Shale::Adapter::<XML adapter>::Node]
29
+ #
30
+ # @api private
31
+ def as_xml(doc)
32
+ element = doc.create_element('xs:element')
33
+
34
+ attributes.each do |name, value|
35
+ doc.add_attribute(element, name, value)
36
+ end
37
+
38
+ unless @required
39
+ doc.add_attribute(element, 'minOccurs', 0)
40
+ end
41
+
42
+ if @collection
43
+ doc.add_attribute(element, 'maxOccurs', 'unbounded')
44
+ end
45
+
46
+ unless @default.nil?
47
+ doc.add_attribute(element, 'default', @default)
48
+ end
49
+
50
+ element
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shale
4
+ module Schema
5
+ class XML
6
+ # Class representing XML Schema <import> element
7
+ #
8
+ # @api private
9
+ class Import
10
+ # Return namespace
11
+ #
12
+ # @return [String]
13
+ #
14
+ # @api private
15
+ attr_reader :namespace
16
+
17
+ # Initialize Import object
18
+ #
19
+ # @param [String, nil] namespace
20
+ # @param [String, nil] location
21
+ #
22
+ # @api private
23
+ def initialize(namespace, location)
24
+ @namespace = namespace
25
+ @location = location
26
+ end
27
+
28
+ # Append element to the XML document
29
+ #
30
+ # @param [Shale::Adapter::<XML adapter>::Document] doc
31
+ #
32
+ # @return [Shale::Adapter::<XML adapter>::Node]
33
+ #
34
+ # @api private
35
+ def as_xml(doc)
36
+ import = doc.create_element('xs:import')
37
+
38
+ doc.add_attribute(import, 'namespace', @namespace)
39
+ doc.add_attribute(import, 'schemaLocation', @location)
40
+
41
+ import
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'attribute'
4
+
5
+ module Shale
6
+ module Schema
7
+ class XML
8
+ # Class representing XML Schema <attribute ref=""> element
9
+ # with a reference
10
+ #
11
+ # @api private
12
+ class RefAttribute < Attribute
13
+ # Initialize RefAttribute object
14
+ #
15
+ # @param [String] ref
16
+ # @param [String, nil] default
17
+ #
18
+ # @api private
19
+ def initialize(ref:, default: nil)
20
+ super(default)
21
+ @ref = ref
22
+ end
23
+
24
+ private
25
+
26
+ # Return attributes as Ruby Hash
27
+ #
28
+ # @return [Hash]
29
+ #
30
+ # @api private
31
+ def attributes
32
+ { 'ref' => @ref }
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end