djc 0.4.0 → 1.1.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 (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: []