shale 0.3.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/README.md +200 -21
  4. data/exe/shaleb +108 -36
  5. data/lib/shale/adapter/nokogiri/document.rb +87 -0
  6. data/lib/shale/adapter/nokogiri/node.rb +100 -0
  7. data/lib/shale/adapter/nokogiri.rb +11 -151
  8. data/lib/shale/adapter/ox/document.rb +80 -0
  9. data/lib/shale/adapter/ox/node.rb +88 -0
  10. data/lib/shale/adapter/ox.rb +9 -134
  11. data/lib/shale/adapter/rexml/document.rb +88 -0
  12. data/lib/shale/adapter/rexml/node.rb +99 -0
  13. data/lib/shale/adapter/rexml.rb +9 -150
  14. data/lib/shale/attribute.rb +6 -0
  15. data/lib/shale/error.rb +39 -0
  16. data/lib/shale/mapper.rb +8 -6
  17. data/lib/shale/schema/compiler/boolean.rb +21 -0
  18. data/lib/shale/schema/compiler/complex.rb +88 -0
  19. data/lib/shale/schema/compiler/date.rb +21 -0
  20. data/lib/shale/schema/compiler/float.rb +21 -0
  21. data/lib/shale/schema/compiler/integer.rb +21 -0
  22. data/lib/shale/schema/compiler/property.rb +70 -0
  23. data/lib/shale/schema/compiler/string.rb +21 -0
  24. data/lib/shale/schema/compiler/time.rb +21 -0
  25. data/lib/shale/schema/compiler/value.rb +21 -0
  26. data/lib/shale/schema/compiler/xml_complex.rb +50 -0
  27. data/lib/shale/schema/compiler/xml_property.rb +73 -0
  28. data/lib/shale/schema/json_compiler.rb +331 -0
  29. data/lib/shale/schema/{json → json_generator}/base.rb +2 -2
  30. data/lib/shale/schema/{json → json_generator}/boolean.rb +1 -1
  31. data/lib/shale/schema/{json → json_generator}/collection.rb +2 -2
  32. data/lib/shale/schema/{json → json_generator}/date.rb +1 -1
  33. data/lib/shale/schema/{json → json_generator}/float.rb +1 -1
  34. data/lib/shale/schema/{json → json_generator}/integer.rb +1 -1
  35. data/lib/shale/schema/{json → json_generator}/object.rb +5 -2
  36. data/lib/shale/schema/{json → json_generator}/ref.rb +1 -1
  37. data/lib/shale/schema/{json → json_generator}/schema.rb +7 -5
  38. data/lib/shale/schema/{json → json_generator}/string.rb +1 -1
  39. data/lib/shale/schema/{json → json_generator}/time.rb +1 -1
  40. data/lib/shale/schema/json_generator/value.rb +23 -0
  41. data/lib/shale/schema/{json.rb → json_generator.rb} +36 -36
  42. data/lib/shale/schema/xml_compiler.rb +919 -0
  43. data/lib/shale/schema/{xml → xml_generator}/attribute.rb +1 -1
  44. data/lib/shale/schema/{xml → xml_generator}/complex_type.rb +5 -2
  45. data/lib/shale/schema/{xml → xml_generator}/element.rb +1 -1
  46. data/lib/shale/schema/{xml → xml_generator}/import.rb +1 -1
  47. data/lib/shale/schema/{xml → xml_generator}/ref_attribute.rb +1 -1
  48. data/lib/shale/schema/{xml → xml_generator}/ref_element.rb +1 -1
  49. data/lib/shale/schema/{xml → xml_generator}/schema.rb +5 -5
  50. data/lib/shale/schema/{xml → xml_generator}/typed_attribute.rb +1 -1
  51. data/lib/shale/schema/{xml → xml_generator}/typed_element.rb +1 -1
  52. data/lib/shale/schema/{xml.rb → xml_generator.rb} +25 -26
  53. data/lib/shale/schema.rb +44 -5
  54. data/lib/shale/type/{composite.rb → complex.rb} +34 -22
  55. data/lib/shale/utils.rb +42 -7
  56. data/lib/shale/version.rb +1 -1
  57. data/lib/shale.rb +8 -19
  58. data/shale.gemspec +1 -1
  59. metadata +47 -27
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'complex'
4
+
5
+ module Shale
6
+ module Schema
7
+ module Compiler
8
+ # Class representing Shale's XML complex type
9
+ #
10
+ # @api private
11
+ class XMLComplex < Complex
12
+ # Accessor for root attribute
13
+ #
14
+ # @return [String]
15
+ #
16
+ # @api private
17
+ attr_accessor :root
18
+
19
+ # Return namespace prefix
20
+ #
21
+ # @return [String]
22
+ #
23
+ # @api private
24
+ attr_reader :prefix
25
+
26
+ # Return namespace URI
27
+ #
28
+ # @return [String]
29
+ #
30
+ # @api private
31
+ attr_reader :namespace
32
+
33
+ # Initialize object
34
+ #
35
+ # @param [String] id
36
+ # @param [String] name
37
+ # @param [String] prefix
38
+ # @param [String] namespace
39
+ #
40
+ # @api private
41
+ def initialize(id, name, prefix, namespace)
42
+ super(id, name)
43
+ @root = name
44
+ @prefix = prefix
45
+ @namespace = namespace
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'property'
4
+
5
+ module Shale
6
+ module Schema
7
+ module Compiler
8
+ # Class representing Shale's property
9
+ #
10
+ # @api private
11
+ class XMLProperty < Property
12
+ # Return namespace URI
13
+ #
14
+ # @return [String]
15
+ #
16
+ # @api private
17
+ attr_reader :namespace
18
+
19
+ # Return namespace prefix
20
+ #
21
+ # @return [String]
22
+ #
23
+ # @api private
24
+ attr_reader :prefix
25
+
26
+ # Initialize object
27
+ #
28
+ # @param [String] name
29
+ # @param [Shale::Schema::Compiler::Type] type
30
+ # @param [true, false] collection
31
+ # @param [Object] default
32
+ # @param [String] prefix
33
+ # @param [String] namespace
34
+ # @param [Symbol] category
35
+ #
36
+ # @api private
37
+ def initialize(name:, type:, collection:, default:, prefix:, namespace:, category:)
38
+ super(name, type, collection, default)
39
+ @prefix = prefix
40
+ @namespace = namespace
41
+ @category = category
42
+ end
43
+
44
+ # Check if property's category is content
45
+ #
46
+ # @return [true, false]
47
+ #
48
+ # @api private
49
+ def content?
50
+ @category == :content
51
+ end
52
+
53
+ # Check if property's category is attribute
54
+ #
55
+ # @return [true, false]
56
+ #
57
+ # @api private
58
+ def attribute?
59
+ @category == :attribute
60
+ end
61
+
62
+ # Check if property's category is element
63
+ #
64
+ # @return [true, false]
65
+ #
66
+ # @api private
67
+ def element?
68
+ @category == :element
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,331 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'uri'
5
+
6
+ require_relative '../../shale'
7
+ require_relative 'compiler/boolean'
8
+ require_relative 'compiler/complex'
9
+ require_relative 'compiler/date'
10
+ require_relative 'compiler/float'
11
+ require_relative 'compiler/integer'
12
+ require_relative 'compiler/property'
13
+ require_relative 'compiler/string'
14
+ require_relative 'compiler/time'
15
+ require_relative 'compiler/value'
16
+
17
+ module Shale
18
+ module Schema
19
+ # Class for compiling JSON schema into Ruby data model
20
+ #
21
+ # @api public
22
+ class JSONCompiler
23
+ # Default root type name
24
+ # @api private
25
+ DEFAULT_ROOT_NAME = 'root'
26
+
27
+ # Shale model template
28
+ # @api private
29
+ MODEL_TEMPLATE = ERB.new(<<~TEMPLATE, trim_mode: '-')
30
+ require 'shale'
31
+ <%- unless type.references.empty? -%>
32
+
33
+ <%- type.references.each do |property| -%>
34
+ require_relative '<%= property.type.file_name %>'
35
+ <%- end -%>
36
+ <%- end -%>
37
+
38
+ class <%= type.name %> < Shale::Mapper
39
+ <%- type.properties.each do |property| -%>
40
+ attribute :<%= property.attribute_name %>, <%= property.type.name -%>
41
+ <%- if property.collection? %>, collection: true<% end -%>
42
+ <%- unless property.default.nil? %>, default: -> { <%= property.default %> }<% end %>
43
+ <%- end -%>
44
+
45
+ json do
46
+ <%- type.properties.each do |property| -%>
47
+ map '<%= property.mapping_name %>', to: :<%= property.attribute_name %>
48
+ <%- end -%>
49
+ end
50
+ end
51
+ TEMPLATE
52
+
53
+ # Generate Shale models from JSON Schema and return them as a Ruby Array of objects
54
+ #
55
+ # @param [Array<String>] schemas
56
+ # @param [String, nil] root_name
57
+ #
58
+ # @raise [SchemaError] when JSON Schema has errors
59
+ #
60
+ # @return [Array<Shale::Schema::Compiler::Complex>]
61
+ #
62
+ # @example
63
+ # Shale::Schema::JSONCompiler.new.as_models([schema1, schema2])
64
+ #
65
+ # @api public
66
+ def as_models(schemas, root_name: nil)
67
+ schemas = schemas.map do |schema|
68
+ Shale.json_adapter.load(schema)
69
+ end
70
+
71
+ @root_name = root_name
72
+ @schema_repository = {}
73
+ @types = []
74
+
75
+ schemas.each do |schema|
76
+ disassemble_schema(schema)
77
+ end
78
+
79
+ compile(schemas[0], true)
80
+
81
+ total_duplicates = Hash.new(0)
82
+ duplicates = Hash.new(0)
83
+
84
+ @types.each do |type|
85
+ total_duplicates[type.name] += 1
86
+ end
87
+
88
+ @types.each do |type|
89
+ duplicates[type.name] += 1
90
+
91
+ if total_duplicates[type.name] > 1
92
+ type.name = format("#{type.name}%d", duplicates[type.name])
93
+ end
94
+ end
95
+
96
+ @types.reverse
97
+ end
98
+
99
+ # Generate Shale models from JSON Schema
100
+ #
101
+ # @param [Array<String>] schemas
102
+ # @param [String, nil] root_name
103
+ #
104
+ # @raise [SchemaError] when JSON Schema has errors
105
+ #
106
+ # @return [Hash<String, String>]
107
+ #
108
+ # @example
109
+ # Shale::Schema::JSONCompiler.new.to_models([schema1, schema2])
110
+ #
111
+ # @api public
112
+ def to_models(schemas, root_name: nil)
113
+ types = as_models(schemas, root_name: root_name)
114
+
115
+ types.to_h do |type|
116
+ [type.file_name, MODEL_TEMPLATE.result(binding)]
117
+ end
118
+ end
119
+
120
+ private
121
+
122
+ # Generate JSON Schema id
123
+ #
124
+ # @param [String, nil] base_id
125
+ # @param [String, nil] id
126
+ #
127
+ # @return [String]
128
+ #
129
+ # @api private
130
+ def build_id(base_id, id)
131
+ return base_id unless id
132
+
133
+ base_uri = URI(base_id || '')
134
+ uri = URI(id)
135
+
136
+ if base_uri.relative? && uri.relative?
137
+ uri.to_s
138
+ else
139
+ base_uri.merge(uri).to_s
140
+ end
141
+ end
142
+
143
+ # Generate JSON pointer
144
+ #
145
+ # @param [String, nil] id
146
+ # @param [Array<String>] fragment
147
+ #
148
+ # @return [String]
149
+ #
150
+ # @api private
151
+ def build_pointer(id, fragment)
152
+ ([id, ['#', *fragment].join('/')] - ['#']).join
153
+ end
154
+
155
+ # Resolve JSON Schem ref
156
+ #
157
+ # @param [String, nil] base_id
158
+ # @param [String, nil] ref
159
+ #
160
+ # @raise [SchemaError] when ref can't be resolved
161
+ #
162
+ # @return [Hash, true, false]
163
+ #
164
+ # @api private
165
+ def resolve_ref(base_id, ref)
166
+ ref_id, fragment = (ref || '').split('#')
167
+ id = build_id(base_id, ref_id == '' ? nil : ref_id)
168
+ key = [id, fragment].compact.join('#')
169
+
170
+ entry = @schema_repository[key]
171
+
172
+ unless entry
173
+ raise SchemaError, "can't resolve reference '#{key}'"
174
+ end
175
+
176
+ if entry[:schema].key?('$ref')
177
+ resolve_ref(id, entry[:schema]['$ref'])
178
+ else
179
+ entry
180
+ end
181
+ end
182
+
183
+ # Get Shale type from JSON Schema type
184
+ #
185
+ # @param [Hash, true, false, nil] schema
186
+ # @param [String] id
187
+ # @param [String] name
188
+ #
189
+ # @return [Shale::Schema::Compiler::Type]
190
+ #
191
+ # @api private
192
+ def infer_type(schema, id, name)
193
+ return unless schema
194
+ return Compiler::Value.new if schema == true
195
+
196
+ type = schema['type']
197
+ format = schema['format']
198
+
199
+ if type.is_a?(Array)
200
+ type -= ['null']
201
+
202
+ if type.length > 1
203
+ return Compiler::Value.new
204
+ else
205
+ type = type[0]
206
+ end
207
+ end
208
+
209
+ if type == 'object'
210
+ Compiler::Complex.new(id, name)
211
+ elsif type == 'string' && format == 'date'
212
+ Compiler::Date.new
213
+ elsif type == 'string' && format == 'date-time'
214
+ Compiler::Time.new
215
+ elsif type == 'string'
216
+ Compiler::String.new
217
+ elsif type == 'number'
218
+ Compiler::Float.new
219
+ elsif type == 'integer'
220
+ Compiler::Integer.new
221
+ elsif type == 'boolean'
222
+ Compiler::Boolean.new
223
+ else
224
+ Compiler::Value.new
225
+ end
226
+ end
227
+
228
+ # Disassemble JSON schema into separate subschemas
229
+ #
230
+ # @param [String] schema
231
+ # @param [Array<String>] fragment
232
+ # @param [String] base_id
233
+ #
234
+ # @raise [SchemaError] when there are problems with JSON schema
235
+ #
236
+ # @api private
237
+ def disassemble_schema(schema, fragment = [], base_id = '')
238
+ schema_key = fragment[-1]
239
+
240
+ if schema.is_a?(Hash) && schema.key?('$id')
241
+ id = build_id(base_id, schema['$id'])
242
+ fragment = []
243
+ else
244
+ id = base_id
245
+ end
246
+
247
+ pointer = build_pointer(id, fragment)
248
+
249
+ if @schema_repository.key?(pointer)
250
+ raise SchemaError, "schema with id '#{pointer}' already exists"
251
+ else
252
+ @schema_repository[pointer] = {
253
+ id: pointer,
254
+ key: schema_key,
255
+ schema: schema,
256
+ }
257
+ end
258
+
259
+ return unless schema.is_a?(Hash)
260
+
261
+ ['properties', '$defs'].each do |definitions|
262
+ (schema[definitions] || {}).each do |subschema_key, subschema|
263
+ disassemble_schema(subschema, [*fragment, definitions, subschema_key], id)
264
+ end
265
+ end
266
+ end
267
+
268
+ # Compile JSON schema into Shale types
269
+ #
270
+ # @param [String] schema
271
+ # @param [true, false] is_root
272
+ # @param [String] base_id
273
+ # @param [Array<String>] fragment
274
+ #
275
+ # @return [Shale::Schema::Compiler::Property, nil]
276
+ #
277
+ # @api private
278
+ def compile(schema, is_root, base_id = '', fragment = [])
279
+ entry = {}
280
+ key = fragment[-1]
281
+ collection = false
282
+ default = nil
283
+
284
+ if schema.is_a?(Hash) && schema.key?('$id')
285
+ id = build_id(base_id, schema['$id'])
286
+ fragment = []
287
+ else
288
+ id = base_id
289
+ end
290
+
291
+ if schema.is_a?(Hash) && schema['type'] == 'array'
292
+ collection = true
293
+ schema = schema['items']
294
+ schema ||= true
295
+ end
296
+
297
+ if schema.is_a?(Hash) && schema.key?('$ref')
298
+ entry = resolve_ref(id, schema['$ref'])
299
+ schema = entry[:schema]
300
+ fragment = entry[:id].split('/') - ['#']
301
+ end
302
+
303
+ pointer = entry[:id] || build_pointer(id, fragment)
304
+
305
+ if is_root
306
+ name = @root_name || entry[:key] || key || DEFAULT_ROOT_NAME
307
+ else
308
+ name = entry[:key] || key
309
+ end
310
+
311
+ type = @types.find { |e| e.id == pointer }
312
+ type ||= infer_type(schema, pointer, name)
313
+
314
+ if schema.is_a?(Hash) && schema.key?('default')
315
+ default = schema['default']
316
+ end
317
+
318
+ if type.is_a?(Compiler::Complex) && !@types.include?(type)
319
+ @types << type
320
+
321
+ (schema['properties'] || {}).each do |subschema_key, subschema|
322
+ property = compile(subschema, false, id, [*fragment, 'properties', subschema_key])
323
+ type.add_property(property) if property
324
+ end
325
+ end
326
+
327
+ Compiler::Property.new(key, type, collection, default) if type
328
+ end
329
+ end
330
+ end
331
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Shale
4
4
  module Schema
5
- class JSON
5
+ class JSONGenerator
6
6
  # Base class for JSON Schema types
7
7
  #
8
8
  # @api private
@@ -30,7 +30,7 @@ module Shale
30
30
  # @api private
31
31
  def as_json
32
32
  type = as_type
33
- type['type'] = [type['type'], 'null'] if type['type'] && @nullable
33
+ type['type'] = [*type['type'], 'null'] if type['type'] && @nullable
34
34
  type['default'] = @default unless @default.nil?
35
35
 
36
36
  type
@@ -4,7 +4,7 @@ require_relative 'base'
4
4
 
5
5
  module Shale
6
6
  module Schema
7
- class JSON
7
+ class JSONGenerator
8
8
  # Class representing JSON Schema boolean type
9
9
  #
10
10
  # @api private
@@ -2,14 +2,14 @@
2
2
 
3
3
  module Shale
4
4
  module Schema
5
- class JSON
5
+ class JSONGenerator
6
6
  # Class representing array type in JSON Schema
7
7
  #
8
8
  # @api private
9
9
  class Collection
10
10
  # Initialize Collection object
11
11
  #
12
- # @param [Shale::Schema::JSON::Base] type
12
+ # @param [Shale::Schema::JSONGenerator::Base] type
13
13
  #
14
14
  # @api private
15
15
  def initialize(type)
@@ -4,7 +4,7 @@ require_relative 'base'
4
4
 
5
5
  module Shale
6
6
  module Schema
7
- class JSON
7
+ class JSONGenerator
8
8
  # Class representing JSON Schema date type
9
9
  #
10
10
  # @api private
@@ -4,7 +4,7 @@ require_relative 'base'
4
4
 
5
5
  module Shale
6
6
  module Schema
7
- class JSON
7
+ class JSONGenerator
8
8
  # Class representing JSON Schema float type
9
9
  #
10
10
  # @api private
@@ -4,7 +4,7 @@ require_relative 'base'
4
4
 
5
5
  module Shale
6
6
  module Schema
7
- class JSON
7
+ class JSONGenerator
8
8
  # Class representing JSON Schema integer type
9
9
  #
10
10
  # @api private
@@ -4,7 +4,7 @@ require_relative 'base'
4
4
 
5
5
  module Shale
6
6
  module Schema
7
- class JSON
7
+ class JSONGenerator
8
8
  # Class representing JSON Schema object type
9
9
  #
10
10
  # @api private
@@ -12,7 +12,10 @@ module Shale
12
12
  # Initialize object
13
13
  #
14
14
  # @param [String] name
15
- # @param [Array<Shale::Schema::JSON::Base, Shale::Schema::JSON::Collection>] properties
15
+ # @param [
16
+ # Array<Shale::Schema::JSONGenerator::Base,
17
+ # Shale::Schema::JSONGenerator::Collection>
18
+ # ] properties
16
19
  #
17
20
  # @api private
18
21
  def initialize(name, properties)
@@ -4,7 +4,7 @@ require_relative 'base'
4
4
 
5
5
  module Shale
6
6
  module Schema
7
- class JSON
7
+ class JSONGenerator
8
8
  # Class representing JSON Schema reference
9
9
  #
10
10
  # @api private
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Shale
4
4
  module Schema
5
- class JSON
5
+ class JSONGenerator
6
6
  # Class representing JSON schema
7
7
  #
8
8
  # @api private
@@ -13,14 +13,15 @@ module Shale
13
13
 
14
14
  # Initialize Schema object
15
15
  #
16
- # @param [Array<Shale::Schema::JSON::Base>] types
16
+ # @param [Array<Shale::Schema::JSONGenerator::Base>] types
17
17
  # @param [String, nil] id
18
18
  # @param [String, nil] description
19
19
  #
20
20
  # @api private
21
- def initialize(types, id: nil, description: nil)
21
+ def initialize(types, id: nil, title: nil, description: nil)
22
22
  @types = types
23
23
  @id = id
24
+ @title = title
24
25
  @description = description
25
26
  end
26
27
 
@@ -29,13 +30,14 @@ module Shale
29
30
  # @return [Hash]
30
31
  #
31
32
  # @example
32
- # Shale::Schema::JSON::Schema.new(types).as_json
33
+ # Shale::Schema::JSONGenerator::Schema.new(types).as_json
33
34
  #
34
35
  # @api private
35
36
  def as_json
36
37
  schema = {
37
38
  '$schema' => DIALECT,
38
- 'id' => @id,
39
+ '$id' => @id,
40
+ 'title' => @title,
39
41
  'description' => @description,
40
42
  }
41
43
 
@@ -4,7 +4,7 @@ require_relative 'base'
4
4
 
5
5
  module Shale
6
6
  module Schema
7
- class JSON
7
+ class JSONGenerator
8
8
  # Class representing JSON Schema string type
9
9
  #
10
10
  # @api private
@@ -4,7 +4,7 @@ require_relative 'base'
4
4
 
5
5
  module Shale
6
6
  module Schema
7
- class JSON
7
+ class JSONGenerator
8
8
  # Class representing JSON Schema time type
9
9
  #
10
10
  # @api private
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module Shale
6
+ module Schema
7
+ class JSONGenerator
8
+ # Class representing JSON Schema any type
9
+ #
10
+ # @api private
11
+ class Value < Base
12
+ # Return JSON Schema fragment as Ruby Hash
13
+ #
14
+ # @return [Hash]
15
+ #
16
+ # @api private
17
+ def as_type
18
+ { 'type' => %w[boolean integer number object string] }
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end