djc 0.4.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/README.md +12 -8
  2. data/lib/djc.rb +295 -224
  3. metadata +4 -3
data/README.md CHANGED
@@ -1,12 +1,16 @@
1
- djconvolvs
2
- ==========
1
+ djc
2
+ ===
3
3
 
4
- JSON to CSV mapping DSL
4
+ Finally, a *D*SL that converts *J*SON into *C*SV
5
5
 
6
- DJ
7
- CSV
8
- OL
9
- N
6
+ ---
7
+
8
+ DJC.build(objects/files) do
9
+ dsl(headers) do
10
+ field do
11
+ +captured_field
12
+ end
13
+ end
14
+ end
10
15
 
11
- ==========
12
16
 
data/lib/djc.rb CHANGED
@@ -1,312 +1,383 @@
1
1
  require 'json'
2
2
  require 'csv'
3
+ require 'ctx'
3
4
 
4
5
  module DJC
5
6
 
6
- class ::String
7
- def ~@
8
- "##{self}"
7
+ class Mapper
8
+
9
+ class << self
10
+ def map(objects, &block)
11
+ self.new(&block).map(objects)
12
+ end
9
13
  end
10
14
 
11
- end
15
+ class Mapping
16
+ attr_accessor :left, :right, :matchers, :field
17
+ def initialize(left, right, field = nil) @left, @right, @field, @matchers = left, right, field, {} end
18
+ def to_s() "MERGE #@right INTO #@left #{ "%(#{field}) " if field }WHERE #{@matchers.map { |k, v| "#{k} = #{v}" }.join(" AND ")}" end
19
+ end
12
20
 
13
- class ::Array
21
+ class ::String
22
+ ctx :mapping do
23
+ def ~@() "~#{self}" end
24
+ def -@() "^.#{self}" end
14
25
 
15
- def select_first(&block)
16
- selected = nil
17
- each do |item|
18
- selected = block.call(item)
19
- break if selected
26
+ def &(other)
27
+ ctx[:mappings] ||= []
28
+ if other.is_a?(Mapping)
29
+ other.left = self
30
+ ctx[:mappings] << other
31
+ else
32
+ ctx[:mappings] << Mapping.new(self, other)
33
+ end
34
+ # p ctx[:mappings]
35
+ self
36
+ end
37
+ def +(other) self & other end
38
+ def <=(other) self & other end
39
+ def <<(other) self & other end
40
+ def <(other) self & other end
41
+
42
+ def %(other) Mapping.new(nil, self, other) end
43
+ def <=>(other) ctx[:mappings].last.matchers[self] = other end
44
+ def method_missing(other) "#{self}.#{other}" end
20
45
  end
21
- selected
22
46
  end
47
+ def method_missing(name) name.to_s end
23
48
 
24
- def walk(obj)
25
- path = self.dup
26
- keys = path.shift.to_s.split('|')
49
+ class ::Class
50
+ ctx :mapping do
51
+ def const_missing(name)
52
+ name
53
+ end
54
+ end
55
+ end
27
56
 
28
- val = keys.select_first do |key|
29
- if obj.is_a? Array
30
- if key == '*'
31
- if path.empty?
32
- obj
33
- else
34
- sub = obj.map do |inner|
35
- path.dup.walk(inner)
57
+ attr_accessor :mappings
58
+ def initialize(&block)
59
+ ctx :mapping do
60
+ self.instance_eval(&block)
61
+ self.mappings = ctx[:mappings]
62
+ end
63
+ end
64
+
65
+ def map(objects)
66
+ objects = Mobj::Circle.wrap(objects)
67
+ mappings.each do |mapping|
68
+ matched = {}
69
+ mapping.matchers.each_pair do |left, right|
70
+ mapping.left.tokenize.walk(objects).each do |lobj|
71
+ lval = left.tokenize.walk(lobj)
72
+ mapping.right.tokenize.walk(objects).each do |robj|
73
+ rval = right.tokenize.walk(robj)
74
+ if lval == rval
75
+ matched[left] ||= []
76
+ matched[left] << [lobj, robj]
77
+ matched[left].uniq!
36
78
  end
37
- path.clear
38
- sub
39
- end
40
- elsif /^[\d,\+-\.]+$/.match(key)
41
- locators = key.split(',').map do |dex|
42
- range = /(?<start>\d+)(?:-|\+|\.\.(?<exclusive>\.)?)?(?<end>-?\d+)/.match(dex)
43
- range ? Range.new(range['start'].to_i, (range['end'] || -1).to_i, range['exclusive']) : dex.to_i
44
79
  end
45
- selected = obj.values_at(*locators)
46
- selected.size == 1 ? selected.first : selected
47
80
  end
48
- elsif obj.is_a? Hash
49
- match = key[/\/(.*)\//, 1]
50
- if match.nil?
51
- obj[key]
81
+ end
82
+
83
+ matching = matched.values.inject(matched.values.first) { |memo, val| memo & val }
84
+ matching.each do |val|
85
+ if mapping.field
86
+ val.first[mapping.field] = val.last
52
87
  else
53
- found = obj.keys.select { |k| Regexp.new(match).match(k) }
54
- found = found.map { |k| path.empty? ? obj[k] : path.walk(obj[k]) }
55
- path.clear
56
- found = found.first if found.size < 2
57
- found
88
+ val.first.merge!(val.last)
58
89
  end
59
- elsif obj.respond_to? key
60
- obj.send(key)
61
90
  end
62
91
  end
63
92
 
64
- path.empty? ? val : path.walk(val)
93
+ objects
65
94
  end
95
+ end
66
96
 
67
- def collate
68
- collated = [[]]
69
- fill = {}
70
- each_with_index do |obj, index|
71
- if obj.is_a?(Array)
72
- obj.each_with_index do |item, row|
73
- collated[row] ||= []
74
- collated[row][index] = item
75
- end
76
- end
77
- end
78
- collated.each do |row|
79
- each_with_index do |obj, index|
80
- unless obj.is_a?(Array)
81
- row[index] = obj
82
- end
83
- end
97
+ class Builder
98
+ class Rule
99
+ attr_accessor :type, :args, :block, :chain
100
+ def initialize(type, *args, &block) @type, @args, @block, @chain = type.sym, args, block, [] end
101
+ def to_s() "RULE:#{type.upcase}(#{args.join(', ')})#{ " +" + chain.map(&:to_s).join(" +") unless chain.empty?}" end
102
+ def method_missing(type, *args, &block)
103
+ chain << Rule.new(type, args, &block)
104
+ self
84
105
  end
85
- collated.size == 1 ? collated.first : collated
86
106
  end
87
107
 
108
+ def initialize(&block)
109
+ ctx :compile do
110
+ self.instance_eval(&block)
111
+ end
112
+ end
88
113
 
89
- def cross
90
- crossed = [[]]
114
+ def map(&block) @mappings = block end
115
+ alias :mappings :map
91
116
 
92
- each do |obj|
93
- if obj.is_a?(Array)
94
- adding = []
95
- obj.each_with_index do |item, index|
96
- crossed.each do |cross|
97
- row = cross.dup
98
- row << item
99
- adding << row
100
- end
101
- end
102
- crossed = adding
103
- else
104
- crossed.each { |cross| cross << obj }
117
+ def rules(*headers, &block)
118
+ @headers = headers
119
+ @dsl = DSL.new(&block)
120
+ end
121
+ alias :dsl :rules
122
+
123
+ def build(objects)
124
+ mapped = Mapper.map(objects, &@mappings)
125
+ rows = @dsl.parse(mapped)
126
+ keys = @headers || rows.flat_map(&:keys).uniq.sort
127
+ CSV.generate do |csv|
128
+ csv << keys
129
+ rows.each do |row|
130
+ csv << keys.map { |key| row[key] }
105
131
  end
106
132
  end
107
- crossed.size == 1 ? crossed.first : crossed
108
133
  end
109
134
  end
110
135
 
111
- class Rule
112
- def parse(paths)
113
- regex = /\/[^\/]+\//
114
- lookup = /\<[^\<]\>/
115
- indexes = /(?:-?\d+(?:(?:\.\.\.?|-|\+)(?:-?\d+)?)?,?)+/
116
- node = /[^\[\]\{\}\.]+/
117
- paths.split('||').map do |path|
118
- path.scan(/#{regex}|#{lookup}|#{indexes}|#{node}/)
119
- end
120
-
136
+ def self.build(objects, &block)
137
+ parsed = [*objects].inject({}) do |memo, (key, val)|
138
+ memo[key.sym] = if val.is_a?(String)
139
+ val = File.read(val) if File.exists?(val)
140
+ JSON.parse(val, max_nesting: false,
141
+ symbolize_names: true,
142
+ create_additions: false,
143
+ object_class: Mobj::CircleHash,
144
+ array_class: Mobj::CircleRay)
145
+ else
146
+ val
147
+ end
148
+ memo
121
149
  end
122
150
 
123
- attr_reader :type, :paths, :blocks
124
- def initialize(type, rules, &block)
125
- if rules.is_a?(String) && rules[0] == '#'
126
- @type, @blocks, @paths = 'LITERAL', [proc { rules[1..-1] }], nil
127
- else
128
- @type, @blocks, @paths = type, [block].compact, (rules.is_a?(String) ? parse(rules) : rules)
151
+ Builder.new(&block).build(parsed)
152
+ end
153
+
154
+ class DSL < ::Object
155
+ class ::String
156
+ ctx(:djc_dsl_def) do
157
+ def +@()
158
+ +ctx[:dsl].__djc_dsl(self.to_sym)
159
+ end
160
+ def ~@()
161
+ ctx[:dsl].find(self)
162
+ end
129
163
  end
130
164
  end
131
165
 
132
- def to_s
133
- rules = if type == 'LITERAL'
134
- @blocks.first.call
135
- elsif paths
136
- paths.join(paths.first.is_a?(Rule) ? ' + ' : '.')
137
- end
138
-
139
- "#{type}(#{rules})"
166
+ def emit_name(name = nil)
167
+ if @name.is_a?(Proc)
168
+ @name.call(name)
169
+ elsif @finder && name
170
+ name
171
+ else
172
+ "#@name#{ "_#{name}" if name }"
173
+ end
140
174
  end
141
-
142
- def sum
143
- @type = 'SUM'
144
- @blocks << proc { |array| array.map(&:to_i).inject(0, :+) if array }
175
+ def __djc_build_name(rule = nil) @name ? "#{@name}_#{rule}" : rule end
176
+ def __djc_reparent(parent) @parent = parent end
177
+
178
+ def initialize(rule = nil, parent = nil, name = parent.attempt(rule).__djc_build_name(rule), &block)
179
+ @rule, @parent, @name, @capture, @finder, @nodes, @composer, @splatter, @partials = rule, parent, name, false, false, [], [], false, {}
180
+ ctx(:djc_dsl_def) do
181
+ ctx[:dsl] = self
182
+ instance_eval(&block)
183
+ end if block
145
184
  self
146
185
  end
147
-
148
- def avg
149
- @type = 'AVG'
150
- @blocks << proc { |array| ( array.map(&:to_i).inject(0.0, :+) / array.size) if array }
186
+ def ~@()
187
+ @finder = true
151
188
  self
152
189
  end
153
-
154
- def each(&each_block)
155
- @type = 'EACH'
156
- @blocks << proc { |array| array.map { |val| each_block.call(val) } if array }
190
+ def +@()
191
+ @capture = true
192
+ @rule = @rule.gsub(/^\/([^()]*)\/$/, '/(\1)/') if @finder
157
193
  self
158
194
  end
159
-
160
- def join(sep = '')
161
- @type = 'JOIN'
162
- @blocks << proc { |vals| vals.is_a?(Array) ? vals.compact.join(sep) : vals }
195
+ def >(other)
196
+ @name = other
163
197
  self
164
198
  end
199
+ alias_method :%, :>
200
+
201
+ def to_str() to_s end
202
+ def to_s(depth = 0)
203
+ str = "DJC:"
204
+ str += @capture ? "+" : ""
205
+ str += (@rule || "ROOT").to_s
206
+ str += "(#@name)" if @name
207
+ str += " [#{@partials.keys.join(",")}]" unless @partials.empty?
208
+ str += " {\n#{@nodes.map {|n| (" " * (depth + 1)) + n.to_s(depth + 1)}.join("\n") }\n#{" " * depth}}" unless @nodes.empty?
209
+ str
210
+ end
165
211
 
166
- def sort(&sort_block)
167
- @type = 'SORT'
168
- @blocks << proc { |sort| sort.is_a?(Array) ? sort.compact.sort(&sort_block) : (sort.nil? ? nil : sort.sort(&sort_block)) }
169
- self
212
+ def find(rule, &block)
213
+ rule = rule.inspect if rule.is_a?(Regexp)
214
+ ~__djc_dsl(rule, &block)
170
215
  end
216
+ alias_method :match, :find
217
+ alias_method :with, :find
218
+ alias_method :field, :find
171
219
 
172
- def match(matcher)
173
- @type = 'MATCH'
174
- @blocks << proc do |val|
175
- if val
176
- if val.is_a?(Array)
177
- val.compact.map do |v|
178
- match = v.scan(matcher).flatten
179
- match.size == 1 ? match.first : match
180
- end
181
- else
182
- match = val.scan(matcher).flatten
183
- match.size == 1 ? match.first : match
184
- end
185
- end
220
+ def compose(*args, &block)
221
+ if ctx[:dsl] == self
222
+ __djc_dsl(:compose, *args, &block)
223
+ else
224
+ @composer << block
225
+ self
186
226
  end
187
- self
188
227
  end
189
228
 
190
- def apply(obj)
191
- if @blocks.empty?
192
- walker = paths.dup
193
- value = nil
194
- while value.nil? && (path = walker.shift)
195
- value = path.is_a?(Rule) ? path.apply(obj) : path.walk(obj)
196
- end
197
- value
229
+ def join(delimiter=$,, &block)
230
+ if ctx[:dsl] == self
231
+ args = delimiter != $, ? [delimiter] : []
232
+ __djc_dsl(:join, *args, &block)
198
233
  else
199
- if paths.nil? || paths.empty?
200
- value = blocks.inject(obj) { |val, block| block.call(val) }
201
- elsif paths.length > 1
202
- value = [[]]
203
- paths.each_with_index do |rule, index|
204
- val = rule.apply(obj)
205
- if val.is_a?(Array)
206
- val.each_with_index do |v, row|
207
- value[row] ||= []
208
- value[row][index] ||= []
209
- value[row][index] = v
210
- end
211
- else
212
- value.first << val
213
- end
214
- end
215
- value = value.map { |val| blocks.inject(val) { |v, block| block.call(v) }} unless blocks.empty?
216
- value = value.first if value.length == 1
217
- value
218
- else
219
- value = paths.first.apply(obj)
220
- value = blocks.inject(value) { |val, block| block.call(val) } unless blocks.empty?
221
- end
234
+ compose { |*values| [*values].join(delimiter) }
235
+ self
222
236
  end
223
- value
224
237
  end
225
- end
226
238
 
227
- class Column
228
- attr_reader :name, :rule
229
- def initialize(name, rule)
230
- @name, @rule = name, rule.is_a?(Rule) ? rule : Rule.new('LOOKUP', rule)
239
+ def sum(initial = 0.0, op = :+, &block)
240
+ if ctx[:dsl] == self
241
+ args = initial != 0.0 ? [initial] : []
242
+ __djc_dsl(:sum, *args, &block)
243
+ else
244
+ compose { |*values| values.map(&:to_f).inject(initial, block ? block : op) if values }
245
+ self
246
+ end
231
247
  end
232
- end
233
248
 
234
- class Builder
235
- def self.compile(path=nil, &block)
236
- builder = Builder.new(path)
237
- builder.instance_eval &block
238
- builder
249
+ def avg(*args, &block)
250
+ if ctx[:dsl] == self
251
+ __djc_dsl(:avg, *args, &block)
252
+ else
253
+ compose { |*values| (values.map(&:to_f).inject(0.0, :+) / values.size) if values }
254
+ self
255
+ end
239
256
  end
240
257
 
241
- def initialize(path = nil)
242
- @path = Rule.new('USING', path) if path
258
+ def sort(*args, &block)
259
+ if ctx[:dsl] == self
260
+ __djc_dsl(:sort, *args, &block)
261
+ else
262
+ compose { |*sort| sort.compact.sort(&block) }
263
+ self
264
+ end
243
265
  end
244
266
 
245
- attr_reader :columns
246
- def []=(column, rule)
247
- @columns ||= []
248
- @columns << Column.new(column, rule)
267
+ def uniq(*args, &block)
268
+ if ctx[:dsl] == self
269
+ __djc_dsl(:uniq, *args, &block)
270
+ else
271
+ compose { |*values| values.uniq }
272
+ self
273
+ end
249
274
  end
250
275
 
251
- def header
252
- columns.map { |column| column.name }
276
+ def count(compact = false, &block)
277
+ if ctx[:dsl] == self
278
+ args = compact ? [compact] : []
279
+ __djc_dsl(:count, *args, &block)
280
+ else
281
+ compose { |*values| compact ? values.compact.size : values.size }
282
+ self
283
+ end
253
284
  end
254
285
 
255
- def sum(*paths)
256
- with(*paths).sum
286
+ def *(*)
287
+ @splatter = true
288
+ self
257
289
  end
258
290
 
259
- def avg(*paths)
260
- with(*paths).avg
291
+ def capture(regex = nil, *captures, &block)
292
+ if ctx[:dsl] == self
293
+ args = [regex, *captures].compact
294
+ __djc_dsl(:capture, *args, &block)
295
+ else
296
+ compose do |value|
297
+ if (match = regex.match(value.to_s))
298
+ if captures.empty?
299
+ block ? block.call(match) : match.captures
300
+ else
301
+ symbols = captures.any? { |i| i.is_a?(String) || i.is_a?(Symbol) }
302
+ captured = symbols ? captures.map { |name| match[name] } : match.to_a.values_at(*captures)
303
+ block ? block.call(*captured) : captured
304
+ end.sequester
305
+ end
306
+ end
307
+ self
308
+ end
261
309
  end
262
310
 
263
- def each(*paths, &block)
264
- with(*paths).each(&block)
311
+ def __djc_partial(name)
312
+ @partials[name] || @parent.__djc_partial(name)
265
313
  end
266
314
 
267
- def with(*paths, &block)
268
- Rule.new('WITH', paths.map { |path| Rule.new('LOOKUP', path) }, &block)
315
+ def __djc_dsl(name, *args, &block)
316
+ if name.to_s[0] == '_'
317
+ if block
318
+ @partials[name] = block
319
+ self
320
+ else
321
+ dsl = __djc_partial(name).call(*args)
322
+ dsl.__djc_reparent(self)
323
+ @nodes << dsl
324
+ dsl
325
+ end
326
+ else
327
+ dsl = DSL.new(name, self, *args, &block)
328
+ @nodes << dsl
329
+ dsl
330
+ end
269
331
  end
270
332
 
271
- def rule(&block)
272
- Rule.new('RULE', nil, &block)
333
+ def method_missing(name, *args, &block)
334
+ __djc_dsl(name, *args, &block)
273
335
  end
274
336
 
275
- def build(json)
276
- json = @path.apply(json) if @path
277
- rows = []
278
- if json.is_a? Array
279
- json.each do |row|
280
- row = @columns.map do |column|
281
- column.rule.apply(row)
282
- end
283
- rows << row
337
+ def rule_parse(data)
338
+ if data.is_a?(Array)
339
+ data.flat_map do |element|
340
+ rule_parse(element)
284
341
  end
285
342
  else
286
- rows << @columns.map do |column|
287
- column.rule.apply(json)
343
+ values = @rule.to_s.walk(data)
344
+
345
+ row = if (@splatter || @finder) && values.is_a?(Hash)
346
+ values.each.with_object({}) { |(k, v), r| r[emit_name(k)] = v }
347
+ elsif @splatter && values.is_a?(Array)
348
+ values.each.with_index.with_object({}) { |(v, i), r| r[emit_name(i)] = v }
349
+ else
350
+ { emit_name => values }
288
351
  end
352
+
353
+ [ @composer.empty? ? row : row.each.with_object({}) { |(k,v), r| r[k] = @composer.inject(v) { |m,c| c.call(*m) } } ]
289
354
  end
290
- rows
291
355
  end
292
356
 
293
- end
294
-
295
- class << self
296
-
297
- def build(json = nil, &block)
298
- json = JSON.parse(json) if json.is_a?(String)
299
-
300
- builder = Builder.compile(&block)
301
-
302
- out = CSV.generate do |csv|
303
- csv << builder.header
304
- builder.build(json).each do |row|
305
- csv << row
357
+ def parse(data, extract = true)
358
+ if @capture
359
+ rule_parse(data)
360
+ else
361
+ data = if @finder && extract
362
+ @rule.to_s.walk(data)
363
+ elsif data && @rule && extract && data.is_a?(Hash)
364
+ data[@rule]
365
+ else
366
+ data
367
+ end
368
+ if data.is_a?(Array)
369
+ data.flat_map do |element|
370
+ parse(element, false)
371
+ end
372
+ else
373
+ @nodes.inject([]) do |rows, node|
374
+ result = node.parse(data)
375
+ result.flat_map do |res|
376
+ rows.empty? ? res : rows.map { |row| row.merge(res) }
377
+ end
378
+ end
306
379
  end
307
380
  end
308
- out
309
381
  end
310
382
  end
311
-
312
383
  end
metadata CHANGED
@@ -1,15 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: djc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 1.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
8
8
  - Mason
9
+ - Glenna
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2012-10-22 00:00:00.000000000 Z
13
+ date: 2012-12-08 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
16
  name: rspec
@@ -35,7 +36,7 @@ extra_rdoc_files: []
35
36
  files:
36
37
  - lib/djc.rb
37
38
  - README.md
38
- homepage: http://rubygems.org/gems/djc
39
+ homepage: https://github.com/gnovos/djc
39
40
  licenses: []
40
41
  post_install_message:
41
42
  rdoc_options: []