shale 0.2.2 → 0.3.0

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 (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