yadtfp 1.0.2

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.
@@ -0,0 +1,82 @@
1
+ require 'singleton'
2
+
3
+
4
+ module Yadtfp
5
+
6
+ class Configuration
7
+ include Singleton
8
+
9
+
10
+ attr_accessor :filter
11
+ attr_reader :parser, :outputter
12
+
13
+
14
+
15
+ # Initializes `@filter`, `@parser`, and `@outputter` to their defaults based on the following table:
16
+ #
17
+ # filter = '*'
18
+ # parser = :ox
19
+ # outputter = :pretty
20
+ def initialize
21
+ # Path to begin traversing the input XML from
22
+ @filter = '*'
23
+
24
+
25
+
26
+ # Parser
27
+ @parser = :ox
28
+
29
+
30
+
31
+ # Diff result output formatter
32
+ @outputter = :pretty
33
+ end
34
+
35
+
36
+
37
+
38
+
39
+ # Assigns supplied parser after converting it to symbol
40
+ #
41
+ # Raises `ArgumentError` if supplied `parser` does not `respond_to` to `to_sym`
42
+ def parser=(parser)
43
+ raise ArgumentError, "Invalid parser `#{parser}`" if !parser.respond_to?(:to_sym)
44
+
45
+ @parser = parser.to_sym
46
+ end
47
+
48
+
49
+
50
+
51
+
52
+ # Assigns supplied outputter after converting it to symbol
53
+ #
54
+ # Raises `ArgumentError` if supplied `outputter` does not `respond_to` to `to_sym`
55
+ def outputter=(outputter)
56
+ raise ArgumentError, "Invalid outputter `#{outputter}`" if !outputter.respond_to?(:to_sym)
57
+
58
+ @outputter = outputter.to_sym
59
+ end
60
+
61
+
62
+
63
+
64
+
65
+ # Parses options, prepares this configuration instance and return the same.
66
+ #
67
+ # Assigns default values to each configuration attributes as defined in the `initialize` method.
68
+ #
69
+ # Raises `ArugmentError` if `options` is not a hash
70
+ def self.parse_options(options = {})
71
+ raise ArgumentError, "Invalid options hash" if !options.is_a?(::Hash)
72
+
73
+ config = self.instance
74
+ config.filter = options[:filter] || '*'
75
+ config.parser = options[:parser] || :ox
76
+ config.outputter = options[:outputter] || :pretty
77
+
78
+ config
79
+ end
80
+
81
+ end
82
+ end
@@ -0,0 +1,3 @@
1
+ require 'yadtfp/outputters_factory'
2
+ require 'yadtfp/outputters/diffable'
3
+ require 'yadtfp/outputters/pretty'
@@ -0,0 +1,74 @@
1
+ module Yadtfp::Outputters
2
+
3
+
4
+ # Module exists to extract out the common methods in outputters.
5
+ #
6
+ # Include this module `Diffable` in each outputters defined.
7
+ module Diffable
8
+
9
+
10
+
11
+ attr_reader :diff
12
+
13
+
14
+
15
+ # Saves supplied difference array.
16
+ #
17
+ # Parameter `arr` must be an array of difference hashes.
18
+ #
19
+ # Raises `ArgumentError` if supplied `arr` is not an `Array` type.
20
+ def initialize(arr = [])
21
+ throw ArgumentError, "`arr` must be an array" if !arr.is_a?(::Array)
22
+
23
+ @diff = arr
24
+ end
25
+
26
+
27
+
28
+
29
+
30
+ # Returns an array containing all the changes.
31
+ def changes
32
+ by_type('c')
33
+ end
34
+
35
+
36
+
37
+
38
+
39
+ # Returns an array containing all the appends.
40
+ def appends
41
+ by_type('a')
42
+ end
43
+
44
+
45
+
46
+
47
+
48
+ # Returns an array containing all the deletes.
49
+ def deletes
50
+ by_type('d')
51
+ end
52
+
53
+
54
+
55
+
56
+
57
+ private
58
+
59
+
60
+
61
+
62
+ # Filters and returns the subset of difference by type
63
+ #
64
+ # `type` can be one of:
65
+ # `c` - Change
66
+ # `a` - Append
67
+ # `d` - Delete
68
+ def by_type(type)
69
+ @diff.select { |d| d[:type] == type }
70
+ end
71
+
72
+ end
73
+
74
+ end
@@ -0,0 +1,128 @@
1
+ module Yadtfp::Outputters
2
+
3
+ class TypeValuesMismatchError < StandardError; end
4
+
5
+
6
+ class Pretty
7
+
8
+ include Yadtfp::Outputters::Diffable
9
+
10
+
11
+ TYPES = [ 'c', 'a', 'd' ]
12
+
13
+
14
+ DiffValue = ->(value) do
15
+ value.is_a?(::Array) ? value.to_enum.with_index(1).map { |v, i| "\xA#{"\x20" * 5}#{i}.\x20#{v}" }.join : value
16
+ end
17
+
18
+
19
+
20
+
21
+
22
+ def print
23
+ content = TYPES.map { |type| header(type) + body(type) }.join ''
24
+
25
+ $stdout.print "#{content}#{repeatc("\xA", 1)}#{summary}"
26
+ end
27
+
28
+
29
+
30
+
31
+
32
+ # Returns header information for given type
33
+ #
34
+ # Type can be one of either Change `c`, Append `a` or Delete `d`:
35
+ #
36
+ # Returns empty string if type is not one of TYPES ('c', 'a' or 'd')
37
+ def header(type)
38
+ return '' if !TYPES.include?(type)
39
+
40
+ header = "Changes (Replace left value with right value)" if type === 'c'
41
+ header = "Appends (Add values to left)" if type === 'a'
42
+ header = "Deletes (Remove values from left)" if type === 'd'
43
+
44
+ header << "\xA#{ "-" * header.length }\xA"
45
+ header
46
+ end
47
+
48
+
49
+
50
+
51
+
52
+ # Returns body for given type
53
+ #
54
+ # Type can be one of either Change `c`, Append `a` or Delete `d`:
55
+ #
56
+ # Returns empty string if type is not one of `TYPES ('c', 'a' or 'd')
57
+ #
58
+ # Raises TypeValuesMismatchError if type is 'd' but diff contains `nil` left
59
+ # Raises TypeValuesMismatchError if type is 'a' but diff contains `nil` right
60
+ def body(type)
61
+ return '' if !TYPES.include?(type)
62
+
63
+ diffs = case type
64
+ when 'c'
65
+ changes
66
+ when 'a'
67
+ appends
68
+ when 'd'
69
+ deletes
70
+ end
71
+
72
+
73
+ if diffs.any? { |d| d[:lvalue].nil? } && type == 'd'
74
+ raise Yadtfp::Outputters::TypeValuesMismatchError, "Unexpected Left value"
75
+ end
76
+
77
+
78
+ if diffs.any? { |d| d[:rvalue].nil? } && type == 'a'
79
+ raise Yadtfp::Outputters::TypeValuesMismatchError, "Unexpected Right value"
80
+ end
81
+
82
+
83
+ body = diffs.to_enum.with_index(1).inject("") do |body, (diff, index)|
84
+ body << "\xA#{index}.\x20Path:\x20#{diff[:path]}"
85
+ body << "\xA#{repeatc("\x20", 3)}Left:\x20#{DiffValue.call(diff[:lvalue])}" if diff.key?(:lvalue)
86
+ body << "\xA#{repeatc("\x20", 3)}Right:\x20#{DiffValue.call(diff[:rvalue])}" if diff.key?(:rvalue)
87
+ body << "\xA"
88
+ end
89
+
90
+ body << "\xA"
91
+ end
92
+
93
+
94
+
95
+
96
+
97
+ # Returns summary of diff
98
+ #
99
+ # Prints total number of differences altogether including changes, appends and deletes.
100
+ def summary
101
+ summary = "Summary of differences"
102
+ summary << "\xA#{ "-" * summary.length }\xA"
103
+
104
+ summary << "Number of differences: #{@diff.length}"
105
+ summary << "\xA Changes 'c': #{changes.length}" if changes.length > 0
106
+ summary << "\xA Appends 'a': #{appends.length}" if appends.length > 0
107
+ summary << "\xA Deletes 'd': #{deletes.length}" if deletes.length > 0
108
+ summary << "\xA"
109
+
110
+ summary
111
+ end
112
+
113
+
114
+
115
+
116
+ private
117
+
118
+
119
+
120
+
121
+ # Repeats character `char` for `length` number of times
122
+ def repeatc(char, length)
123
+ char * length
124
+ end
125
+
126
+ end
127
+
128
+ end
@@ -0,0 +1,26 @@
1
+ module Yadtfp
2
+
3
+ class OutputtersFactory
4
+
5
+ # Creates and returns one of outputters
6
+ #
7
+ # Note that parameter `outputter` needs to be a symbol, and diff needs to be an array.
8
+ # The allowed values for `outputter` are:
9
+ # :pretty
10
+ #
11
+ # Raises `ArgumentError` if `outputter` is not one of the values specified above.
12
+ def self.create(outputter, diff)
13
+
14
+ case outputter
15
+ when :pretty
16
+ return Outputters::Pretty.new(diff)
17
+
18
+ end
19
+
20
+ raise ArgumentError, "Invalid outputter `#{outputter}`"
21
+ end
22
+
23
+
24
+ end
25
+
26
+ end
@@ -0,0 +1 @@
1
+ require 'yadtfp/parsers_factory'
@@ -0,0 +1,328 @@
1
+ require 'ox'
2
+ require 'pathname'
3
+
4
+ module Yadtfp::Parsers
5
+
6
+
7
+ class UnsupportedError < StandardError; end
8
+
9
+
10
+ class Ox
11
+
12
+
13
+ # Parses `xml` into a `::Ox::Document` and returns the same `::Ox::Document`
14
+ #
15
+ # Parameter `xml` is either a file path or a xml string.
16
+ # Method adds `xml` processing instruction with `version: '1.0'` and `encoding: 'UTF-8'`
17
+ #
18
+ # Method returns empty document `::Ox::Document` with processing instruction if supplied `xml` is nil or empty.
19
+ def parse(xml)
20
+ doc = ::Ox::Document.new(version: '1.0', encoding: 'UTF-8')
21
+ return doc if xml.nil? || xml.empty?
22
+
23
+
24
+ file = ::Pathname.new(xml).absolute? ? xml : File.expand_path(xml)
25
+ if File.exists?(file)
26
+ parsed = ::Ox.load_file(file, node: :generic)
27
+ else
28
+ parsed = ::Ox.parse(xml)
29
+ end
30
+ parsed_doc = doc.dup << parsed if parsed.is_a?(::Ox::Element)
31
+
32
+
33
+ # Apply filter
34
+ filtered = filter(parsed_doc) if parsed_doc.is_a?(::Ox::Element)
35
+ filtered.each { |f| doc << f } if filtered.is_a?(::Array)
36
+
37
+ doc
38
+ end
39
+
40
+
41
+
42
+
43
+
44
+ def filter(doc, path = Yadtfp::Configuration.instance.filter)
45
+ raise ArgumentError, "Document cannot be nil" if doc.nil?
46
+
47
+ doc.locate(path)
48
+ end
49
+
50
+
51
+
52
+
53
+
54
+
55
+ # Generates array of difference hashes between left and right.
56
+ #
57
+ # The difference hash contains the following keys:
58
+ # type: Type of difference. Either `c` for change, `a` for append or `d` for delete.
59
+ # path: Path needs to be in the format described in the documentation of
60
+ # [`::Ox::Element#locate`](http://www.ohler.com/ox/Ox/Element.html#locate-instance_method).
61
+ # lvalue: Left value
62
+ # rvalue: Right value
63
+ #
64
+ # Method loads xml document from `left` and `right`.
65
+ # Method parameters `left` and `right` both must be a `::Ox::Document`, each of which can be result of `parse`
66
+ # method.
67
+ #
68
+ # Method returns `[]` if both `left` or `right` are `nil` or both are equal.
69
+ # Method returns `right` translated into difference hash if `left` is `nil`.
70
+ # Method returns `left` translated into difference hash if `right` is `nil`.
71
+ def diff(left, right)
72
+ return [] if left == right || (left.nil? && right.nil?)
73
+
74
+
75
+ left = flat_h(left)
76
+ right = flat_h(right)
77
+
78
+
79
+ return right.values if left.nil? && right.is_a?(::Hash)
80
+ return left.values if right.nil? && left.is_a?(::Hash)
81
+
82
+
83
+ return flat_h_diff(left, right)
84
+ end
85
+
86
+
87
+
88
+
89
+
90
+
91
+
92
+ # `flat_h`: Flat hash
93
+ #
94
+ # Returns hash of xml document. Traverses xml document recursively and generates a flat hash.
95
+ #
96
+ # Each element is converted to a hash where `key` is path to the node, attribute, comment or CDATA and value is the
97
+ # string or nil value held by the node, attribute, comment or CDATA
98
+ #
99
+ # Example:
100
+ #
101
+ # <xml id='root', name='Foo'>Bar</xml>
102
+ # # => { '/xml/id' => 'root', '/xml/@name' => 'Foo', '/xml' => 'Bar' }
103
+ def flat_h(element)
104
+ raise UnsupportedError, "Multiple root elements are not allowed" if element.is_a?(::Array)
105
+
106
+
107
+ # Nothing to process
108
+ return nil if element.nil?
109
+
110
+
111
+ # Result hash
112
+ result = {}
113
+
114
+
115
+
116
+ # Attributes
117
+ attrs = process_attributes(element) || {}
118
+
119
+ attrs.map do |key, value|
120
+ path = element.value.to_s != '' ? "/#{element.value}/#{key}" : "/#{key}"
121
+ result[path] = value
122
+ end
123
+
124
+
125
+
126
+
127
+
128
+ # Nodes
129
+ if !element.is_a?(::Ox::Comment)
130
+
131
+ if element.nodes.length.zero?
132
+ path = "/#{element.value}"
133
+ result[path] = nil if attrs.length.zero?
134
+ end
135
+
136
+
137
+
138
+
139
+ element.nodes.each do |node|
140
+
141
+ if node.is_a?(::String)
142
+
143
+ path = "/#{element.value}"
144
+
145
+ if result.key?(path)
146
+ result[path] = [ result[path] ] if !result[path].is_a?(::Array)
147
+ result[path] << node
148
+ else
149
+ result[path] = node
150
+ end
151
+
152
+ elsif node.is_a?(::Ox::Comment)
153
+
154
+ path = element.value.to_s != '' ? "/#{element.value}/comment()" : '/comment()'
155
+ result[path] = process_comments(element)
156
+
157
+ elsif node.is_a?(::Ox::CData)
158
+
159
+ path = element.value.to_s != '' ? "/#{element.value}/cdata()" : '/cdata()'
160
+ result[path] = process_cdata(element)
161
+
162
+ else
163
+
164
+ n = flat_h(node)
165
+
166
+
167
+ n.each do |key, value|
168
+
169
+ path = element.value.to_s == '' ? "#{key}" : "/#{element.value}#{key}"
170
+
171
+ if result.key?(path)
172
+
173
+ result[path] = [ result[path] ] if !result[path].is_a?(::Array)
174
+ result[path] = [ result[path] ] if key.end_with?('comment()') || key.end_with?('cdata()')
175
+ result[path] << value
176
+
177
+ else
178
+
179
+ result[path] = value
180
+
181
+ end
182
+
183
+ end
184
+
185
+ end
186
+
187
+ end
188
+
189
+ end
190
+
191
+ result
192
+
193
+ end
194
+
195
+
196
+
197
+
198
+
199
+
200
+
201
+ # Returns array of difference hashes for left and right flat hashes.
202
+ #
203
+ # The resultant array consists of difference hashes in the form of:
204
+ #
205
+ # { type: type, path: path, lvalue: lvalue, rvalue: rvalue }
206
+ #
207
+ # where:
208
+ #
209
+ # `type` - Type of difference, either `c`, `a` or `d`
210
+ # `path` - Path to the node, attribute, cdata, comment.
211
+ # `lvalue` - Value at path in left
212
+ # `rvalue` - Value at path in right
213
+ #
214
+ #
215
+ # Three possible `types` of differences are:
216
+ # `c` - When both left and right contain different values for same path
217
+ # `a` - Append values to left at the specified path. Occurs when path is not present in left.
218
+ # `d` - Delete values from left at the specified path. Occurs when path is not present in right.
219
+ #
220
+ # Returns nil if left and right are equal.
221
+ def flat_h_diff(left, right)
222
+ return nil if left == right
223
+
224
+ diff_hash = ( left.keys | right.keys ).inject({}) do |hash, key|
225
+ type = 'c'
226
+ type = 'd' if !right.key?(key) && left.key?(key)
227
+ type = 'a' if !left.key?(key) && right.key?(key)
228
+
229
+ if left[key] != right[key]
230
+
231
+ if left[key].is_a?(::Array) && right[key].is_a?(::Array)
232
+
233
+ type = 'd' if left[key].length > right[key].length
234
+ type = 'a' if left[key].length < right[key].length
235
+ type = 'c' if (left[key] - right[key]).length >= 1 && (right[key] - left[key]).length >= 1
236
+
237
+
238
+ if (left[key] - right[key]).empty? && (right[key] - left[key]).empty?
239
+ hash[key] = { type: type, path: key, lvalue: left[key].uniq, rvalue: nil } if type == 'd'
240
+ hash[key] = { type: type, path: key, lvalue: nil, rvalue: right[key].uniq } if type == 'a'
241
+
242
+ else
243
+
244
+ hash[key] = { type: type, path: key, lvalue: left[key] - right[key], rvalue: nil } if type == 'd'
245
+ hash[key] = { type: type, path: key, lvalue: nil, rvalue: right[key] - left[key] } if type == 'a'
246
+ hash[key] = { type: type, path: key, lvalue: (left[key] - right[key]), rvalue: (right[key] - left[key]) } if type == 'c'
247
+ end
248
+
249
+ else
250
+
251
+ hash[key] = { type: type, path: key, lvalue: left[key], rvalue: right[key] }
252
+
253
+ end
254
+
255
+ end
256
+
257
+ hash
258
+ end
259
+
260
+ diff_hash.values
261
+ end
262
+
263
+
264
+
265
+
266
+
267
+
268
+ # Process element comments
269
+ #
270
+ # Returns array of comments
271
+ # Returns nil if element does not contain any comment nodes.
272
+ def process_comments(element)
273
+ return if !element.respond_to?(:nodes)
274
+
275
+ comments = element.nodes.dup.inject([]) do |result, element|
276
+ result << element.value if element.is_a?(::Ox::Comment) && !element.value.empty?
277
+ result
278
+ end
279
+
280
+ return comments if comments.length >= 1
281
+ nil
282
+ end
283
+
284
+
285
+
286
+
287
+
288
+
289
+ # Process element cdata
290
+ #
291
+ # Returns array of cdata
292
+ # Returns nil if element does not contain any cdata markup.
293
+ def process_cdata(element)
294
+ return if !element.respond_to?(:nodes)
295
+
296
+ cdata = element.nodes.dup.inject([]) do |result, element|
297
+ result << element.value if element.is_a?(::Ox::CData) && !element.value.empty?
298
+ result
299
+ end
300
+
301
+ return cdata if cdata.length >= 1
302
+ nil
303
+ end
304
+
305
+
306
+
307
+
308
+
309
+
310
+ # Process node attributes
311
+ # Returns hash of node attributes applying `@` prefix to every attribute name
312
+ #
313
+ # Returns nil if node does not contain any attributes
314
+ def process_attributes(node)
315
+ return nil if !node.respond_to?(:attributes)
316
+
317
+ attributes = node.attributes.dup.inject({}) do |hash, (key, value)|
318
+ hash["@#{key}"] = value
319
+ hash
320
+ end
321
+
322
+ return attributes if attributes.length >= 1
323
+ nil
324
+ end
325
+
326
+ end
327
+
328
+ end