shale 0.3.1 → 0.6.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/README.md +398 -41
  4. data/exe/shaleb +108 -36
  5. data/lib/shale/adapter/nokogiri/document.rb +97 -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 +90 -0
  9. data/lib/shale/adapter/ox/node.rb +97 -0
  10. data/lib/shale/adapter/ox.rb +9 -134
  11. data/lib/shale/adapter/rexml/document.rb +98 -0
  12. data/lib/shale/adapter/rexml/node.rb +99 -0
  13. data/lib/shale/adapter/rexml.rb +9 -150
  14. data/lib/shale/adapter/toml_rb.rb +34 -0
  15. data/lib/shale/attribute.rb +6 -0
  16. data/lib/shale/error.rb +56 -0
  17. data/lib/shale/mapper.rb +67 -13
  18. data/lib/shale/mapping/descriptor/xml.rb +10 -1
  19. data/lib/shale/mapping/dict.rb +18 -0
  20. data/lib/shale/mapping/xml.rb +40 -5
  21. data/lib/shale/schema/compiler/boolean.rb +21 -0
  22. data/lib/shale/schema/compiler/complex.rb +88 -0
  23. data/lib/shale/schema/compiler/date.rb +21 -0
  24. data/lib/shale/schema/compiler/float.rb +21 -0
  25. data/lib/shale/schema/compiler/integer.rb +21 -0
  26. data/lib/shale/schema/compiler/property.rb +70 -0
  27. data/lib/shale/schema/compiler/string.rb +21 -0
  28. data/lib/shale/schema/compiler/time.rb +21 -0
  29. data/lib/shale/schema/compiler/value.rb +21 -0
  30. data/lib/shale/schema/compiler/xml_complex.rb +50 -0
  31. data/lib/shale/schema/compiler/xml_property.rb +73 -0
  32. data/lib/shale/schema/json_compiler.rb +331 -0
  33. data/lib/shale/schema/{json → json_generator}/base.rb +2 -2
  34. data/lib/shale/schema/{json → json_generator}/boolean.rb +1 -1
  35. data/lib/shale/schema/{json → json_generator}/collection.rb +2 -2
  36. data/lib/shale/schema/{json → json_generator}/date.rb +1 -1
  37. data/lib/shale/schema/{json → json_generator}/float.rb +1 -1
  38. data/lib/shale/schema/{json → json_generator}/integer.rb +1 -1
  39. data/lib/shale/schema/{json → json_generator}/object.rb +5 -2
  40. data/lib/shale/schema/{json → json_generator}/ref.rb +1 -1
  41. data/lib/shale/schema/{json → json_generator}/schema.rb +6 -4
  42. data/lib/shale/schema/{json → json_generator}/string.rb +1 -1
  43. data/lib/shale/schema/{json → json_generator}/time.rb +1 -1
  44. data/lib/shale/schema/json_generator/value.rb +23 -0
  45. data/lib/shale/schema/{json.rb → json_generator.rb} +36 -36
  46. data/lib/shale/schema/xml_compiler.rb +919 -0
  47. data/lib/shale/schema/{xml → xml_generator}/attribute.rb +1 -1
  48. data/lib/shale/schema/{xml → xml_generator}/complex_type.rb +5 -2
  49. data/lib/shale/schema/{xml → xml_generator}/element.rb +1 -1
  50. data/lib/shale/schema/{xml → xml_generator}/import.rb +1 -1
  51. data/lib/shale/schema/{xml → xml_generator}/ref_attribute.rb +1 -1
  52. data/lib/shale/schema/{xml → xml_generator}/ref_element.rb +1 -1
  53. data/lib/shale/schema/{xml → xml_generator}/schema.rb +5 -5
  54. data/lib/shale/schema/{xml → xml_generator}/typed_attribute.rb +1 -1
  55. data/lib/shale/schema/{xml → xml_generator}/typed_element.rb +1 -1
  56. data/lib/shale/schema/{xml.rb → xml_generator.rb} +25 -26
  57. data/lib/shale/schema.rb +44 -5
  58. data/lib/shale/type/{composite.rb → complex.rb} +156 -51
  59. data/lib/shale/type/value.rb +31 -2
  60. data/lib/shale/utils.rb +42 -7
  61. data/lib/shale/version.rb +1 -1
  62. data/lib/shale.rb +22 -19
  63. data/shale.gemspec +3 -3
  64. metadata +50 -29
data/exe/shaleb CHANGED
@@ -1,14 +1,32 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
+ require 'fileutils'
4
5
  require 'optparse'
5
6
 
6
- base_path = File.expand_path('../lib', __dir__)
7
+ def require_local_or_global(path)
8
+ base_path = File.expand_path('../lib', __dir__)
7
9
 
8
- if File.exist?(base_path)
9
- require_relative '../lib/shale/schema'
10
- else
11
- require 'shale/schema'
10
+ if File.exist?(base_path)
11
+ require_relative "../lib/#{path}"
12
+ else
13
+ require path
14
+ end
15
+ end
16
+
17
+ require_local_or_global('shale/schema')
18
+
19
+ def load_xml_parser
20
+ require_local_or_global('shale/adapter/nokogiri')
21
+ Shale.xml_adapter = Shale::Adapter::Nokogiri
22
+ rescue LoadError
23
+ begin
24
+ require_local_or_global('shale/adapter/rexml')
25
+ Shale.xml_adapter = Shale::Adapter::REXML
26
+ rescue LoadError
27
+ puts "Can't load XML parser. Make sure Nokogiri or REXML is installed on your system!"
28
+ exit
29
+ end
12
30
  end
13
31
 
14
32
  params = {}
@@ -16,11 +34,17 @@ params = {}
16
34
  ARGV << '-h' if ARGV.empty?
17
35
 
18
36
  OptionParser.new do |opts|
19
- opts.banner = "Usage: shaleb [options]\nexample: shaleb -i data_model.rb -c MyRoot"
20
-
21
- opts.on('-i INPUT', '--input', 'Input file')
22
- opts.on('-o OUTPUT', '--output', 'Output file (defaults to STDOUT)')
23
- opts.on('-c CLASS', '--class CLASS', 'Shale model class name')
37
+ opts.banner = <<~BANNER
38
+ Usage: shaleb [options]
39
+ example generate schema from Shale model: shaleb -g -i data_model.rb -c MyRoot
40
+ example generate Shale model from schema: shaleb -c -i schema1.json,schema2.json -c MyRoot
41
+ BANNER
42
+
43
+ opts.on('-g', '--generate', 'generate schema from Shale model')
44
+ opts.on('-c', '--compile', 'compile schema into Shale model')
45
+ opts.on('-i INPUT', '--input', Array, 'Input file')
46
+ opts.on('-o OUTPUT', '--output', 'Output (defaults to STDOUT)')
47
+ opts.on('-r ROOT', '--root ROOT', 'Shale model class name')
24
48
  opts.on('-f FORMAT', '--format FORMAT', 'Schema format: JSON (default), XML')
25
49
  opts.on('-p', '--pretty', 'Pretty print generated schema')
26
50
 
@@ -30,46 +54,94 @@ OptionParser.new do |opts|
30
54
  end
31
55
  end.parse!(into: params)
32
56
 
33
- input_path = File.expand_path(params[:input], Dir.pwd)
34
-
35
- unless File.exist?(input_path)
36
- puts "File '#{input_path}' does not exist"
37
- exit
38
- end
57
+ if params[:compile]
58
+ unless params[:input]
59
+ puts 'Input file is required: shaleb -c -i schema1.json,schema2.json'
60
+ exit
61
+ end
39
62
 
40
- unless params[:class]
41
- puts 'Model class is required'
42
- exit
43
- end
63
+ schemas = params[:input].map do |file|
64
+ path = File.expand_path(file, Dir.pwd)
44
65
 
45
- require input_path
66
+ if File.exist?(path)
67
+ File.read(path)
68
+ else
69
+ puts "File '#{path}' does not exist"
70
+ exit
71
+ end
72
+ end
46
73
 
47
- klass = Object.const_get(params[:class])
74
+ if params[:format] == 'xml'
75
+ load_xml_parser
76
+ models = Shale::Schema.from_xml(schemas)
77
+ else
78
+ models = Shale::Schema.from_json(schemas, root_name: params[:root])
79
+ end
48
80
 
49
- if params[:format] == 'xml'
50
81
  if params[:output]
51
- base_name = File.basename(params[:output], File.extname(params[:output]))
52
- schemas = Shale::Schema.to_xml(klass, base_name, pretty: params[:pretty])
82
+ dir = File.expand_path(params[:output], Dir.pwd)
83
+ FileUtils.mkdir_p(dir) unless File.directory?(dir)
53
84
 
54
- schemas.map do |name, xml|
55
- File.write(File.expand_path(name, Dir.pwd), xml)
85
+ models.each do |name, model|
86
+ output_path = File.join(dir, "#{name}.rb")
87
+ File.write(output_path, model)
56
88
  end
57
89
  else
58
- schemas = Shale::Schema.to_xml(klass, pretty: params[:pretty])
59
-
60
- output = schemas.map do |name, xml|
61
- "<!-- #{name} -->\n#{xml}\n"
90
+ output = models.map do |name, model|
91
+ "# --- #{name}.rb ---\n#{model}\n"
62
92
  end.join("\n")
63
93
 
64
94
  puts output
65
95
  end
66
96
  else
67
- schema = Shale::Schema.to_json(klass, pretty: params[:pretty])
97
+ unless params[:input]
98
+ puts 'Input file is required: shaleb -i model.rb -r MyClass'
99
+ exit
100
+ end
68
101
 
69
- if params[:output]
70
- output_path = File.expand_path(params[:output], Dir.pwd)
71
- File.write(output_path, schema)
102
+ input_path = File.expand_path(params[:input][0], Dir.pwd)
103
+
104
+ unless File.exist?(input_path)
105
+ puts "File '#{input_path}' does not exist"
106
+ exit
107
+ end
108
+
109
+ unless params[:root]
110
+ puts 'Model class is required: shaleb -i model.rb -r MyClass'
111
+ exit
112
+ end
113
+
114
+ require input_path
115
+
116
+ klass = Object.const_get(params[:root])
117
+
118
+ if params[:format] == 'xml'
119
+ load_xml_parser
120
+
121
+ if params[:output]
122
+ base_name = File.basename(params[:output], File.extname(params[:output]))
123
+ schemas = Shale::Schema.to_xml(klass, base_name, pretty: params[:pretty])
124
+
125
+ schemas.map do |name, xml|
126
+ File.write(File.expand_path(name, Dir.pwd), xml)
127
+ end
128
+ else
129
+ schemas = Shale::Schema.to_xml(klass, pretty: params[:pretty])
130
+
131
+ output = schemas.map do |name, xml|
132
+ "<!-- #{name} -->\n#{xml}\n"
133
+ end.join("\n")
134
+
135
+ puts output
136
+ end
72
137
  else
73
- puts schema
138
+ schema = Shale::Schema.to_json(klass, pretty: params[:pretty])
139
+
140
+ if params[:output]
141
+ output_path = File.expand_path(params[:output], Dir.pwd)
142
+ File.write(output_path, schema)
143
+ else
144
+ puts schema
145
+ end
74
146
  end
75
147
  end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shale
4
+ module Adapter
5
+ module Nokogiri
6
+ # Wrapper around Nokogiri API
7
+ #
8
+ # @api private
9
+ class Document
10
+ # Initialize object
11
+ #
12
+ # @api private
13
+ def initialize
14
+ @doc = ::Nokogiri::XML::Document.new
15
+ @namespaces = {}
16
+ end
17
+
18
+ # Return Nokogiri document
19
+ #
20
+ # @return [::Nokogiri::XML::Document]
21
+ #
22
+ # @api private
23
+ def doc
24
+ if @doc.root
25
+ @namespaces.each do |prefix, namespace|
26
+ @doc.root.add_namespace(prefix, namespace)
27
+ end
28
+ end
29
+
30
+ @doc
31
+ end
32
+
33
+ # Create Nokogiri element
34
+ #
35
+ # @param [String] name Name of the XML element
36
+ #
37
+ # @return [::Nokogiri::XML::Element]
38
+ #
39
+ # @api private
40
+ def create_element(name)
41
+ ::Nokogiri::XML::Element.new(name, @doc)
42
+ end
43
+
44
+ # Create CDATA node and add it to parent
45
+ #
46
+ # @param [String] text
47
+ # @param [::Nokogiri::XML::Element] parent
48
+ #
49
+ # @api private
50
+ def create_cdata(text, parent)
51
+ parent.add_child(::Nokogiri::XML::CDATA.new(@doc, text))
52
+ end
53
+
54
+ # Add XML namespace to document
55
+ #
56
+ # @param [String] prefix
57
+ # @param [String] namespace
58
+ #
59
+ # @api private
60
+ def add_namespace(prefix, namespace)
61
+ @namespaces[prefix] = namespace if prefix && namespace
62
+ end
63
+
64
+ # Add attribute to Nokogiri element
65
+ #
66
+ # @param [::Nokogiri::XML::Element] element Nokogiri element
67
+ # @param [String] name Name of the XML attribute
68
+ # @param [String] value Value of the XML attribute
69
+ #
70
+ # @api private
71
+ def add_attribute(element, name, value)
72
+ element[name] = value
73
+ end
74
+
75
+ # Add child element to Nokogiri element
76
+ #
77
+ # @param [::Nokogiri::XML::Element] element Nokogiri parent element
78
+ # @param [::Nokogiri::XML::Element] child Nokogiri child element
79
+ #
80
+ # @api private
81
+ def add_element(element, child)
82
+ element.add_child(child)
83
+ end
84
+
85
+ # Add text node to Nokogiri element
86
+ #
87
+ # @param [::Nokogiri::XML::Element] element Nokogiri element
88
+ # @param [String] text Text to add
89
+ #
90
+ # @api private
91
+ def add_text(element, text)
92
+ element.content = text
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shale
4
+ module Adapter
5
+ module Nokogiri
6
+ # Wrapper around Nokogiri::XML::Node API
7
+ #
8
+ # @api private
9
+ class Node
10
+ # Initialize object with Nokogiri node
11
+ #
12
+ # @param [::Nokogiri::XML::Node] node Nokogiri node
13
+ #
14
+ # @api private
15
+ def initialize(node)
16
+ @node = node
17
+ end
18
+
19
+ # Return namespaces defined on document
20
+ #
21
+ # @return [Hash<String, String>]
22
+ #
23
+ # @example
24
+ # node.namespaces # => { 'foo' => 'http://foo.com', 'bar' => 'http://bar.com' }
25
+ #
26
+ # @api private
27
+ def namespaces
28
+ @node.namespaces.transform_keys { |e| e.sub('xmlns:', '') }
29
+ end
30
+
31
+ # Return name of the node in the format of
32
+ # namespace:name when the node is namespaced or just name when it's not
33
+ #
34
+ # @return [String]
35
+ #
36
+ # @example without namespace
37
+ # node.name # => Bar
38
+ #
39
+ # @example with namespace
40
+ # node.name # => http://foo:Bar
41
+ #
42
+ # @api private
43
+ def name
44
+ [@node.namespace&.href, @node.name].compact.join(':')
45
+ end
46
+
47
+ # Return all attributes associated with the node
48
+ #
49
+ # @return [Hash]
50
+ #
51
+ # @api private
52
+ def attributes
53
+ @node.attribute_nodes.each_with_object({}) do |node, hash|
54
+ name = [node.namespace&.href, node.name].compact.join(':')
55
+ hash[name] = node.value
56
+ end
57
+ end
58
+
59
+ # Return node's parent
60
+ #
61
+ # @return [Shale::Adapter::Nokogiri::Node, nil]
62
+ #
63
+ # @api private
64
+ def parent
65
+ if @node.respond_to?(:parent) && @node.parent && @node.parent.name != 'document'
66
+ self.class.new(@node.parent)
67
+ end
68
+ end
69
+
70
+ # Return node's element children
71
+ #
72
+ # @return [Array<Shale::Adapter::Nokogiri::Node>]
73
+ #
74
+ # @api private
75
+ def children
76
+ @node
77
+ .children
78
+ .to_a
79
+ .filter(&:element?)
80
+ .map { |e| self.class.new(e) }
81
+ end
82
+
83
+ # Return first text child of a node
84
+ #
85
+ # @return [String]
86
+ #
87
+ # @api private
88
+ def text
89
+ first = @node
90
+ .children
91
+ .to_a
92
+ .filter { |e| e.text? || e.cdata? }
93
+ .first
94
+
95
+ first&.text
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -2,6 +2,10 @@
2
2
 
3
3
  require 'nokogiri'
4
4
 
5
+ require_relative '../error'
6
+ require_relative 'nokogiri/document'
7
+ require_relative 'nokogiri/node'
8
+
5
9
  module Shale
6
10
  module Adapter
7
11
  # Nokogiri adapter
@@ -12,7 +16,9 @@ module Shale
12
16
  #
13
17
  # @param [String] xml XML document
14
18
  #
15
- # @return [::Nokogiri::XML::Document]
19
+ # @raise [ParseError] when XML document has errors
20
+ #
21
+ # @return [Shale::Adapter::Nokogiri::Node]
16
22
  #
17
23
  # @api private
18
24
  def self.load(xml)
@@ -20,6 +26,10 @@ module Shale
20
26
  config.noblanks
21
27
  end
22
28
 
29
+ unless doc.errors.empty?
30
+ raise ParseError, "Document is invalid: #{doc.errors}"
31
+ end
32
+
23
33
  Node.new(doc.root)
24
34
  end
25
35
 
@@ -57,156 +67,6 @@ module Shale
57
67
  def self.create_document
58
68
  Document.new
59
69
  end
60
-
61
- # Wrapper around Nokogiri API
62
- #
63
- # @api private
64
- class Document
65
- # Initialize object
66
- #
67
- # @api private
68
- def initialize
69
- @doc = ::Nokogiri::XML::Document.new
70
- @namespaces = {}
71
- end
72
-
73
- # Return Nokogiri document
74
- #
75
- # @return [::Nokogiri::XML::Document]
76
- #
77
- # @api private
78
- def doc
79
- if @doc.root
80
- @namespaces.each do |prefix, namespace|
81
- @doc.root.add_namespace(prefix, namespace)
82
- end
83
- end
84
-
85
- @doc
86
- end
87
-
88
- # Create Nokogiri element
89
- #
90
- # @param [String] name Name of the XML element
91
- #
92
- # @return [::Nokogiri::XML::Element]
93
- #
94
- # @api private
95
- def create_element(name)
96
- ::Nokogiri::XML::Element.new(name, @doc)
97
- end
98
-
99
- # Add XML namespace to document
100
- #
101
- # @param [String] prefix
102
- # @param [String] namespace
103
- #
104
- # @api private
105
- def add_namespace(prefix, namespace)
106
- @namespaces[prefix] = namespace if prefix && namespace
107
- end
108
-
109
- # Add attribute to Nokogiri element
110
- #
111
- # @param [::Nokogiri::XML::Element] element Nokogiri element
112
- # @param [String] name Name of the XML attribute
113
- # @param [String] value Value of the XML attribute
114
- #
115
- # @api private
116
- def add_attribute(element, name, value)
117
- element[name] = value
118
- end
119
-
120
- # Add child element to Nokogiri element
121
- #
122
- # @param [::Nokogiri::XML::Element] element Nokogiri parent element
123
- # @param [::Nokogiri::XML::Element] child Nokogiri child element
124
- #
125
- # @api private
126
- def add_element(element, child)
127
- element.add_child(child)
128
- end
129
-
130
- # Add text node to Nokogiri element
131
- #
132
- # @param [::Nokogiri::XML::Element] element Nokogiri element
133
- # @param [String] text Text to add
134
- #
135
- # @api private
136
- def add_text(element, text)
137
- element.content = text
138
- end
139
- end
140
-
141
- # Wrapper around Nokogiri::XML::Node API
142
- #
143
- # @api private
144
- class Node
145
- # Initialize object with Nokogiri node
146
- #
147
- # @param [::Nokogiri::XML::Node] node Nokogiri node
148
- #
149
- # @api private
150
- def initialize(node)
151
- @node = node
152
- end
153
-
154
- # Return name of the node in the format of
155
- # namespace:name when the node is namespaced or just name when it's not
156
- #
157
- # @return [String]
158
- #
159
- # @example without namespace
160
- # node.name # => Bar
161
- #
162
- # @example with namespace
163
- # node.name # => http://foo:Bar
164
- #
165
- # @api private
166
- def name
167
- [@node.namespace&.href, @node.name].compact.join(':')
168
- end
169
-
170
- # Return all attributes associated with the node
171
- #
172
- # @return [Hash]
173
- #
174
- # @api private
175
- def attributes
176
- @node.attribute_nodes.each_with_object({}) do |node, hash|
177
- name = [node.namespace&.href, node.name].compact.join(':')
178
- hash[name] = node.value
179
- end
180
- end
181
-
182
- # Return node's element children
183
- #
184
- # @return [Array<Shale::Adapter::Nokogiri::Node>]
185
- #
186
- # @api private
187
- def children
188
- @node
189
- .children
190
- .to_a
191
- .filter(&:element?)
192
- .map { |e| self.class.new(e) }
193
- end
194
-
195
- # Return first text child of a node
196
- #
197
- # @return [String]
198
- #
199
- # @api private
200
- def text
201
- first = @node
202
- .children
203
- .to_a
204
- .filter(&:text?)
205
- .first
206
-
207
- first&.text
208
- end
209
- end
210
70
  end
211
71
  end
212
72
  end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Shale
4
+ module Adapter
5
+ module Ox
6
+ # Wrapper around Ox API
7
+ #
8
+ # @api private
9
+ class Document
10
+ # Return Ox document
11
+ #
12
+ # @return [::Ox::Document]
13
+ #
14
+ # @api private
15
+ attr_reader :doc
16
+
17
+ # Initialize object
18
+ #
19
+ # @api private
20
+ def initialize
21
+ @doc = ::Ox::Document.new
22
+ end
23
+
24
+ # Create Ox element
25
+ #
26
+ # @param [String] name Name of the XML element
27
+ #
28
+ # @return [::Ox::Element]
29
+ #
30
+ # @api private
31
+ def create_element(name)
32
+ ::Ox::Element.new(name)
33
+ end
34
+
35
+ # Create CDATA node and add it to parent
36
+ #
37
+ # @param [String] text
38
+ # @param [::Ox::Element] parent
39
+ #
40
+ # @api private
41
+ def create_cdata(text, parent)
42
+ parent << ::Ox::CData.new(text)
43
+ end
44
+
45
+ # Add XML namespace to document
46
+ #
47
+ # Ox doesn't support XML namespaces so this method does nothing.
48
+ #
49
+ # @param [String] prefix
50
+ # @param [String] namespace
51
+ #
52
+ # @api private
53
+ def add_namespace(prefix, namespace)
54
+ # :noop:
55
+ end
56
+
57
+ # Add attribute to Ox element
58
+ #
59
+ # @param [::Ox::Element] element Ox element
60
+ # @param [String] name Name of the XML attribute
61
+ # @param [String] value Value of the XML attribute
62
+ #
63
+ # @api private
64
+ def add_attribute(element, name, value)
65
+ element[name] = value
66
+ end
67
+
68
+ # Add child element to Ox element
69
+ #
70
+ # @param [::Ox::Element] element Ox parent element
71
+ # @param [::Ox::Element] child Ox child element
72
+ #
73
+ # @api private
74
+ def add_element(element, child)
75
+ element << child
76
+ end
77
+
78
+ # Add text node to Ox element
79
+ #
80
+ # @param [::Ox::Element] element Ox element
81
+ # @param [String] text Text to add
82
+ #
83
+ # @api private
84
+ def add_text(element, text)
85
+ element << text
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end