shale 0.3.1 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
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