djc 0.4.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +12 -8
- data/lib/djc.rb +295 -224
- metadata +4 -3
data/README.md
CHANGED
@@ -1,12 +1,16 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
djc
|
2
|
+
===
|
3
3
|
|
4
|
-
|
4
|
+
Finally, a *D*SL that converts *J*SON into *C*SV
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
7
|
-
|
8
|
-
|
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
|
-
|
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
|
-
|
21
|
+
class ::String
|
22
|
+
ctx :mapping do
|
23
|
+
def ~@() "~#{self}" end
|
24
|
+
def -@() "^.#{self}" end
|
14
25
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
49
|
+
class ::Class
|
50
|
+
ctx :mapping do
|
51
|
+
def const_missing(name)
|
52
|
+
name
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
27
56
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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
|
-
|
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
|
-
|
93
|
+
objects
|
65
94
|
end
|
95
|
+
end
|
66
96
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
90
|
-
|
114
|
+
def map(&block) @mappings = block end
|
115
|
+
alias :mappings :map
|
91
116
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
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
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
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
|
143
|
-
|
144
|
-
|
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
|
-
|
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
|
-
|
155
|
-
@
|
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
|
-
|
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
|
167
|
-
|
168
|
-
|
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
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
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
|
191
|
-
if
|
192
|
-
|
193
|
-
|
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
|
-
|
200
|
-
|
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
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
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
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
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
|
242
|
-
|
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
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
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
|
252
|
-
|
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
|
256
|
-
|
286
|
+
def *(*)
|
287
|
+
@splatter = true
|
288
|
+
self
|
257
289
|
end
|
258
290
|
|
259
|
-
def
|
260
|
-
|
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
|
264
|
-
|
311
|
+
def __djc_partial(name)
|
312
|
+
@partials[name] || @parent.__djc_partial(name)
|
265
313
|
end
|
266
314
|
|
267
|
-
def
|
268
|
-
|
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
|
272
|
-
|
333
|
+
def method_missing(name, *args, &block)
|
334
|
+
__djc_dsl(name, *args, &block)
|
273
335
|
end
|
274
336
|
|
275
|
-
def
|
276
|
-
|
277
|
-
|
278
|
-
|
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
|
-
|
287
|
-
|
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
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
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:
|
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-
|
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:
|
39
|
+
homepage: https://github.com/gnovos/djc
|
39
40
|
licenses: []
|
40
41
|
post_install_message:
|
41
42
|
rdoc_options: []
|