dynarex 1.8.27
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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/lib/dynarex.rb +1558 -0
- metadata +251 -0
- metadata.gz.sig +0 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d331676a235d9cb8422a3855be3803bbf3203ac0f87238efe44c7adf2d541dc4
|
4
|
+
data.tar.gz: 306dfe9c3667fdf27db9e9ac7d410ef1fe25148fe79dd278543109264823112c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3577e9e6067e4fef09f1c31654212a5ce47c3af65dafd9e1e10d887c190ff4ed253ae8859980ea3cfc273a57664da7aa6d58bfbb6b94f996c5efd2fb17a208f0
|
7
|
+
data.tar.gz: 81d4c396ba22264bc517117a6cfc3011a185c26e733f63f25ea42e0b28dc9177ea2788b5bd01282c8bff7e0556a8f9b6f2094b11c326eedad81b5357d65f5571
|
checksums.yaml.gz.sig
ADDED
Binary file
|
data.tar.gz.sig
ADDED
Binary file
|
data/lib/dynarex.rb
ADDED
@@ -0,0 +1,1558 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# file: dynarex.rb
|
4
|
+
|
5
|
+
require 'open-uri'
|
6
|
+
require 'dynarex-import'
|
7
|
+
#require 'line-tree'
|
8
|
+
#require 'rexle'
|
9
|
+
require 'rexle-builder'
|
10
|
+
require 'rexslt'
|
11
|
+
require 'dynarex-xslt'
|
12
|
+
require 'recordx'
|
13
|
+
require 'rxraw-lineparser'
|
14
|
+
require 'yaml'
|
15
|
+
require 'rowx'
|
16
|
+
require 'ostruct'
|
17
|
+
require 'table-formatter'
|
18
|
+
#require 'rxfhelper'
|
19
|
+
require 'kvx'
|
20
|
+
require 'json'
|
21
|
+
|
22
|
+
|
23
|
+
module RegGem
|
24
|
+
|
25
|
+
def self.register()
|
26
|
+
'
|
27
|
+
hkey_gems
|
28
|
+
doctype
|
29
|
+
dynarex
|
30
|
+
require dynarex
|
31
|
+
class Dynarex
|
32
|
+
media_type dynarex
|
33
|
+
'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
class DynarexException < Exception
|
39
|
+
end
|
40
|
+
|
41
|
+
class DynarexRecordset < Array
|
42
|
+
|
43
|
+
def initialize(a, caller=nil)
|
44
|
+
super(a)
|
45
|
+
@caller = caller
|
46
|
+
end
|
47
|
+
|
48
|
+
def reject!()
|
49
|
+
|
50
|
+
a = self.to_a.clone
|
51
|
+
a2 = super
|
52
|
+
return nil unless a2
|
53
|
+
a3 = a - a2
|
54
|
+
|
55
|
+
@caller.delete a3.map(&:id)
|
56
|
+
self
|
57
|
+
end
|
58
|
+
|
59
|
+
def sum(field)
|
60
|
+
self.inject(0) {|r, x| r + x[field.to_sym][/\d+(\.\d+)?$/].to_f }
|
61
|
+
end
|
62
|
+
|
63
|
+
def index(val)
|
64
|
+
self.map(&:to_h).index val.to_h
|
65
|
+
end
|
66
|
+
|
67
|
+
def index_by_id(id)
|
68
|
+
self.map(&:id).index id
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
class Dynarex
|
75
|
+
include RXFHelperModule
|
76
|
+
using ColouredText
|
77
|
+
|
78
|
+
attr_accessor :format_mask, :delimiter, :xslt_schema, :schema, :linked,
|
79
|
+
:order, :type, :limit, :xslt, :json_out, :unique
|
80
|
+
|
81
|
+
|
82
|
+
#Create a new dynarex document from 1 of the following options:
|
83
|
+
#* a local file path
|
84
|
+
#* a URL
|
85
|
+
#* a schema string
|
86
|
+
# Dynarex.new 'contacts[title,description]/contact(name,age,dob)'
|
87
|
+
#* an XML string
|
88
|
+
# Dynarex.new '<contacts><summary><schema>contacts/contact(name,age,dob)</schema></summary><records/></contacts>'
|
89
|
+
|
90
|
+
def initialize(rawx=nil, username: nil, password: nil, schema: nil,
|
91
|
+
default_key: nil, json_out: true, debug: false,
|
92
|
+
delimiter: ' # ', autosave: false, order: 'ascending',
|
93
|
+
unique: false, filepath: nil)
|
94
|
+
|
95
|
+
|
96
|
+
puts 'inside Dynarex::initialize' if debug
|
97
|
+
@username, @password, @schema, @default_key, @json_out, @debug = \
|
98
|
+
username, password, schema, default_key, json_out, debug
|
99
|
+
@autosave, @unique = autosave, unique
|
100
|
+
@local_filepath = filepath
|
101
|
+
|
102
|
+
puts ('@debug: ' + @debug.inspect).debug if debug
|
103
|
+
@delimiter = delimiter
|
104
|
+
@spaces_delimited = false
|
105
|
+
@order = order
|
106
|
+
@limit = nil
|
107
|
+
@records, @flat_records = [], []
|
108
|
+
rawx ||= schema if schema
|
109
|
+
|
110
|
+
if rawx then
|
111
|
+
|
112
|
+
return import(rawx) if rawx =~ /\.txt$/
|
113
|
+
openx(rawx.clone)
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
self.order = @order unless @order.to_sym == :ascending
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
def add(x)
|
122
|
+
@doc.root.add x
|
123
|
+
@dirty_flag = true
|
124
|
+
self
|
125
|
+
end
|
126
|
+
|
127
|
+
def all()
|
128
|
+
|
129
|
+
refresh! if @dirty_flag
|
130
|
+
a = @doc.root.xpath("records/*").map {|x| recordx_to_record x}
|
131
|
+
DynarexRecordset.new(a, self)
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
def clone()
|
136
|
+
Dynarex.new(self.to_xml)
|
137
|
+
end
|
138
|
+
|
139
|
+
def default_key()
|
140
|
+
self.summary[:default_key]
|
141
|
+
end
|
142
|
+
|
143
|
+
def delimiter=(separator)
|
144
|
+
|
145
|
+
if separator == :spaces then
|
146
|
+
@spaces_delimited = true
|
147
|
+
separator = ' # '
|
148
|
+
end
|
149
|
+
|
150
|
+
@delimiter = separator
|
151
|
+
|
152
|
+
if separator.length > 0 then
|
153
|
+
@summary[:delimiter] = separator
|
154
|
+
else
|
155
|
+
@summary.delete :delimiter
|
156
|
+
end
|
157
|
+
|
158
|
+
@format_mask = @format_mask.to_s.gsub(/\s/, separator)
|
159
|
+
@summary[:format_mask] = @format_mask
|
160
|
+
end
|
161
|
+
|
162
|
+
def doc
|
163
|
+
(load_records; rebuild_doc) if @dirty_flag == true
|
164
|
+
@doc
|
165
|
+
end
|
166
|
+
|
167
|
+
# XML import
|
168
|
+
#
|
169
|
+
def foreign_import(options={})
|
170
|
+
o = {xml: '', schema: ''}.merge(options)
|
171
|
+
h = {xml: o[:xml], schema: @schema, foreign_schema: o[:schema]}
|
172
|
+
buffer = DynarexImport.new(h).to_xml
|
173
|
+
|
174
|
+
openx(buffer)
|
175
|
+
self
|
176
|
+
end
|
177
|
+
|
178
|
+
def fields
|
179
|
+
@fields
|
180
|
+
end
|
181
|
+
|
182
|
+
def first
|
183
|
+
r = @doc.root.element("records/*")
|
184
|
+
r ? recordx_to_record(r) : nil
|
185
|
+
end
|
186
|
+
|
187
|
+
def format_mask=(s)
|
188
|
+
@format_mask = s
|
189
|
+
@summary[:format_mask] = @format_mask
|
190
|
+
end
|
191
|
+
|
192
|
+
def insert(raw_params)
|
193
|
+
record = make_record(raw_params)
|
194
|
+
@doc.root.element('records/*').insert_before record
|
195
|
+
@dirty_flag = true
|
196
|
+
end
|
197
|
+
|
198
|
+
def inspect()
|
199
|
+
"<object #%s>" % [self.object_id]
|
200
|
+
end
|
201
|
+
|
202
|
+
def linked=(bool)
|
203
|
+
@linked = bool == 'true'
|
204
|
+
end
|
205
|
+
|
206
|
+
def order=(value)
|
207
|
+
|
208
|
+
self.summary.merge!({order: value.to_s})
|
209
|
+
|
210
|
+
@order = value.to_s
|
211
|
+
end
|
212
|
+
|
213
|
+
def recordx_type()
|
214
|
+
@summary[:recordx_type]
|
215
|
+
end
|
216
|
+
|
217
|
+
def schema=(s)
|
218
|
+
openx s
|
219
|
+
end
|
220
|
+
|
221
|
+
def type=(v)
|
222
|
+
@order = 'descending' if v == 'feed'
|
223
|
+
@type = v
|
224
|
+
@summary[:type] = v
|
225
|
+
end
|
226
|
+
|
227
|
+
# Returns the hash representation of the document summary.
|
228
|
+
#
|
229
|
+
def summary
|
230
|
+
@summary
|
231
|
+
end
|
232
|
+
|
233
|
+
# Return a Hash (which can be edited) containing all records.
|
234
|
+
#
|
235
|
+
def records
|
236
|
+
|
237
|
+
load_records if @dirty_flag == true
|
238
|
+
|
239
|
+
if block_given? then
|
240
|
+
yield(@records)
|
241
|
+
rebuild_doc
|
242
|
+
@dirty_flag = true
|
243
|
+
else
|
244
|
+
@records
|
245
|
+
end
|
246
|
+
|
247
|
+
end
|
248
|
+
|
249
|
+
# Returns a ready-only snapshot of records as a simple Hash.
|
250
|
+
#
|
251
|
+
def flat_records(select: nil)
|
252
|
+
|
253
|
+
fields = select
|
254
|
+
|
255
|
+
load_records if @dirty_flag == true
|
256
|
+
|
257
|
+
if fields then
|
258
|
+
|
259
|
+
case fields.class.to_s.downcase.to_sym
|
260
|
+
when :string
|
261
|
+
field = fields.to_sym
|
262
|
+
@flat_records.map {|row| {field => row[field]}}
|
263
|
+
when :symbol
|
264
|
+
field = fields.to_sym
|
265
|
+
@flat_records.map {|row| {field => row[field]} }
|
266
|
+
when :array
|
267
|
+
@flat_records.map {|row| fields.inject({})\
|
268
|
+
{|r,x| r.merge(x.to_sym => row[x.to_sym])}}
|
269
|
+
end
|
270
|
+
|
271
|
+
else
|
272
|
+
@flat_records
|
273
|
+
end
|
274
|
+
|
275
|
+
end
|
276
|
+
|
277
|
+
alias to_a flat_records
|
278
|
+
|
279
|
+
# Returns an array snapshot of OpenStruct records
|
280
|
+
#
|
281
|
+
def ro_records
|
282
|
+
flat_records.map {|record| OpenStruct.new record }
|
283
|
+
end
|
284
|
+
|
285
|
+
def rm(force: false)
|
286
|
+
|
287
|
+
if force or all.empty? then
|
288
|
+
FileX.rm @local_filepath if @local_filepath
|
289
|
+
'file ' + @local_filepath + ' deleted'
|
290
|
+
else
|
291
|
+
'unable to rm file: document not empty'
|
292
|
+
end
|
293
|
+
|
294
|
+
end
|
295
|
+
|
296
|
+
|
297
|
+
def to_doc
|
298
|
+
self.clone().doc
|
299
|
+
end
|
300
|
+
|
301
|
+
# Typically uses the 1st field as a key and the remaining fields as the value
|
302
|
+
#
|
303
|
+
def to_h()
|
304
|
+
|
305
|
+
key = self.default_key.to_sym
|
306
|
+
fields = self.fields() - [key]
|
307
|
+
puts 'fields: ' + fields.inspect if @debug
|
308
|
+
|
309
|
+
flat_records.inject({}) do |r, h|
|
310
|
+
|
311
|
+
puts 'h: ' + h.inspect if @debug
|
312
|
+
|
313
|
+
value = if h.length == 2 then
|
314
|
+
h[fields[0]].to_s
|
315
|
+
else
|
316
|
+
fields.map {|x| h[x]}
|
317
|
+
end
|
318
|
+
|
319
|
+
r.merge(h[key] => value)
|
320
|
+
end
|
321
|
+
|
322
|
+
end
|
323
|
+
|
324
|
+
def to_html(domain: '')
|
325
|
+
|
326
|
+
h = {username: @username, password: @password}
|
327
|
+
xsl_buffer = RXFHelper.read(domain + @xslt, h).first
|
328
|
+
Rexslt.new(xsl_buffer, self.to_doc).to_s
|
329
|
+
|
330
|
+
end
|
331
|
+
|
332
|
+
|
333
|
+
# to_json: pretty is set to true because although the file size is larger,
|
334
|
+
# the file can be load slightly quicker
|
335
|
+
|
336
|
+
def to_json(pretty: true)
|
337
|
+
|
338
|
+
records = self.to_a
|
339
|
+
summary = self.summary.to_h
|
340
|
+
|
341
|
+
root_name = schema()[/^\w+/]
|
342
|
+
record_name = schema()[/(?<=\/)[^\(]+/]
|
343
|
+
|
344
|
+
h = {
|
345
|
+
root_name.to_sym =>
|
346
|
+
{
|
347
|
+
summary: @summary,
|
348
|
+
records: @records.map {|_, h| {record_name.to_sym => h} }
|
349
|
+
}
|
350
|
+
}
|
351
|
+
|
352
|
+
pretty ? JSON.pretty_generate(h) : h.to_json
|
353
|
+
|
354
|
+
end
|
355
|
+
|
356
|
+
def to_s(header: true, delimiter: @delimiter)
|
357
|
+
|
358
|
+
xsl_buffer =<<EOF
|
359
|
+
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
|
360
|
+
<xsl:output encoding="UTF-8"
|
361
|
+
method="text"
|
362
|
+
indent="no"
|
363
|
+
omit-xml-declaration="yes"/>
|
364
|
+
|
365
|
+
<xsl:template match="*">
|
366
|
+
<xsl:for-each select="records/*">[!regex_values]<xsl:text>
|
367
|
+
</xsl:text>
|
368
|
+
</xsl:for-each>
|
369
|
+
</xsl:template>
|
370
|
+
</xsl:stylesheet>
|
371
|
+
EOF
|
372
|
+
|
373
|
+
|
374
|
+
raw_summary_fields = self.summary[:schema][/^\w+\[([^\]]+)\]/,1]
|
375
|
+
sumry = ''
|
376
|
+
|
377
|
+
if raw_summary_fields then
|
378
|
+
summary_fields = raw_summary_fields.split(',') # .map(&:to_sym)
|
379
|
+
sumry = summary_fields.map {|x| x.strip!; x + ': ' + \
|
380
|
+
self.summary[x.to_sym].to_s}.join("\n") + "\n\n"
|
381
|
+
end
|
382
|
+
|
383
|
+
if @raw_header then
|
384
|
+
declaration = @raw_header
|
385
|
+
else
|
386
|
+
|
387
|
+
smry_fields = %i(schema)
|
388
|
+
smry_fields << :order if self.summary[:order] == 'descending'
|
389
|
+
|
390
|
+
if delimiter.length > 0 then
|
391
|
+
smry_fields << :delimiter
|
392
|
+
else
|
393
|
+
smry_fields << :format_mask unless self.summary[:rawdoc_type] == 'rowx'
|
394
|
+
end
|
395
|
+
s = smry_fields.map {|x| "%s=\"%s\"" % \
|
396
|
+
[x, self.send(x).gsub('"', '\"') ]}.join ' '
|
397
|
+
|
398
|
+
declaration = %Q(<?dynarex %s?>\n) % s
|
399
|
+
end
|
400
|
+
|
401
|
+
docheader = declaration + sumry
|
402
|
+
|
403
|
+
if self.summary[:rawdoc_type] == 'rowx' then
|
404
|
+
a = self.fields.map do |field|
|
405
|
+
"<xsl:if test=\"%s != ''\">
|
406
|
+
<xsl:text>\n</xsl:text>%s:<xsl:text> </xsl:text><xsl:value-of select='%s'/>
|
407
|
+
</xsl:if>" % ([field]*3)
|
408
|
+
end
|
409
|
+
|
410
|
+
puts ('a: ' + a.inspect).debug if @debug
|
411
|
+
|
412
|
+
xslt_format = a.join
|
413
|
+
|
414
|
+
xsl_buffer.sub!(/\[!regex_values\]/, xslt_format)
|
415
|
+
|
416
|
+
if @debug then
|
417
|
+
File.write '/tmp/foo.xsl', xsl_buffer
|
418
|
+
File.write '/tmp/foo.xml', @doc.xml
|
419
|
+
puts 'xsl_buffer: ' + xsl_buffer.inspect
|
420
|
+
end
|
421
|
+
|
422
|
+
out = Rexslt.new(xsl_buffer, @doc).to_s
|
423
|
+
|
424
|
+
docheader + "\n--+\n" + out
|
425
|
+
elsif self.summary[:rawdoc_type] == 'sectionx' then
|
426
|
+
|
427
|
+
a = (self.fields - [:uid, 'uid']).map do |field|
|
428
|
+
"<xsl:if test=\"%s != ''\">
|
429
|
+
<xsl:text>\n</xsl:text><xsl:value-of select='%s'/>
|
430
|
+
</xsl:if>" % ([field]*2)
|
431
|
+
end
|
432
|
+
|
433
|
+
xslt_format = a.join
|
434
|
+
|
435
|
+
xsl_buffer.sub!(/\[!regex_values\]/, xslt_format)
|
436
|
+
puts 'xsl_buffer: ' + xsl_buffer.inspect if @debug
|
437
|
+
|
438
|
+
out = Rexslt.new(xsl_buffer, @doc).to_s
|
439
|
+
|
440
|
+
header ? docheader + "--#\n" + out : out
|
441
|
+
|
442
|
+
elsif self.delimiter.length > 0 then
|
443
|
+
puts 'dinddd'
|
444
|
+
tfo = TableFormatter.new border: false, wrap: false, \
|
445
|
+
divider: self.delimiter
|
446
|
+
tfo.source = self.to_a.map{|x| x.values}
|
447
|
+
docheader + tfo.display.strip
|
448
|
+
|
449
|
+
else
|
450
|
+
|
451
|
+
format_mask = self.format_mask
|
452
|
+
format_mask.gsub!(/\[[^!\]]+\]/) {|x| x[1] }
|
453
|
+
|
454
|
+
s1, s2 = '<xsl:text>', '</xsl:text>'
|
455
|
+
xslt_format = s1 + format_mask\
|
456
|
+
.gsub(/(?:\[!(\w+)\])/, s2 + '<xsl:value-of select="\1"/>' + s1) + s2
|
457
|
+
|
458
|
+
xsl_buffer.sub!(/\[!regex_values\]/, xslt_format)
|
459
|
+
|
460
|
+
puts 'xsl_buffer: ' + xsl_buffer if @debug
|
461
|
+
out = Rexslt.new(xsl_buffer, @doc).to_s
|
462
|
+
|
463
|
+
header ? docheader + "\n" + out : out
|
464
|
+
end
|
465
|
+
|
466
|
+
end
|
467
|
+
|
468
|
+
def to_table(fields: nil, markdown: false, innermarkdown: false, heading: true)
|
469
|
+
|
470
|
+
tfo = TableFormatter.new markdown: markdown, innermarkdown: innermarkdown
|
471
|
+
tfo.source = self.to_a.map {|h| fields ? fields.map {|x| h[x]} : h.values }
|
472
|
+
|
473
|
+
if heading then
|
474
|
+
raw_headings = self.summary[:headings]
|
475
|
+
fields = raw_headings.split(self.delimiter) if raw_headings and fields.nil?
|
476
|
+
tfo.labels = (fields ? fields : self.fields.map{|x| x.to_s.capitalize })
|
477
|
+
end
|
478
|
+
|
479
|
+
tfo
|
480
|
+
|
481
|
+
end
|
482
|
+
|
483
|
+
def to_xml(opt={})
|
484
|
+
opt = {pretty: true} if opt == :pretty
|
485
|
+
display_xml(opt)
|
486
|
+
end
|
487
|
+
|
488
|
+
# Save the document to a file.
|
489
|
+
|
490
|
+
def save(filepath=@local_filepath, options={})
|
491
|
+
|
492
|
+
if @debug then
|
493
|
+
puts 'inside Dynarex::save'
|
494
|
+
puts 'filepath: ' + filepath.inspect
|
495
|
+
|
496
|
+
end
|
497
|
+
|
498
|
+
return unless filepath
|
499
|
+
|
500
|
+
opt = {pretty: true}.merge options
|
501
|
+
|
502
|
+
@local_filepath = filepath || 'dx.xml'
|
503
|
+
xml = display_xml(opt)
|
504
|
+
buffer = block_given? ? yield(xml) : xml
|
505
|
+
|
506
|
+
if @debug then
|
507
|
+
puts 'before write; filepath: ' + filepath.inspect
|
508
|
+
puts 'buffer: ' + buffer.inspect
|
509
|
+
end
|
510
|
+
|
511
|
+
FileX.write filepath, buffer
|
512
|
+
FileX.write(filepath.sub(/\.xml$/,'.json'), self.to_json) if @json_out
|
513
|
+
end
|
514
|
+
|
515
|
+
#Parses 1 or more lines of text to create or update existing records.
|
516
|
+
|
517
|
+
def parse(x=nil)
|
518
|
+
|
519
|
+
@dirty_flag = true
|
520
|
+
|
521
|
+
if x.is_a? Array then
|
522
|
+
|
523
|
+
unless @schema then
|
524
|
+
cols = x.first.keys.map {|c| c == 'id' ? 'uid' : c}
|
525
|
+
self.schema = "items/item(%s)" % cols.join(', ')
|
526
|
+
end
|
527
|
+
|
528
|
+
x.each {|record| self.create record }
|
529
|
+
return self
|
530
|
+
|
531
|
+
end
|
532
|
+
raw_buffer, type = RXFHelper.read(x, auto: false)
|
533
|
+
|
534
|
+
if raw_buffer.is_a? String then
|
535
|
+
|
536
|
+
buffer = block_given? ? yield(raw_buffer) : raw_buffer.clone
|
537
|
+
string_parse buffer
|
538
|
+
|
539
|
+
else
|
540
|
+
foreign_import x
|
541
|
+
end
|
542
|
+
|
543
|
+
end
|
544
|
+
|
545
|
+
|
546
|
+
alias import parse
|
547
|
+
|
548
|
+
#Create a record from a hash containing the field name, and the field value.
|
549
|
+
# dynarex = Dynarex.new 'contacts/contact(name,age,dob)'
|
550
|
+
# dynarex.create name: Bob, age: 52
|
551
|
+
|
552
|
+
def create(obj, id: nil, custom_attributes: {})
|
553
|
+
|
554
|
+
puts 'inside create' if @debug
|
555
|
+
raise 'Dynarex#create(): input error: no arg provided' unless obj
|
556
|
+
|
557
|
+
case obj.class.to_s.downcase.to_sym
|
558
|
+
when :hash
|
559
|
+
hash_create obj, id, attr: custom_attributes
|
560
|
+
when :string
|
561
|
+
create_from_line obj, id, attr: custom_attributes
|
562
|
+
when :recordx
|
563
|
+
hash_create obj.to_h, id || obj.id, attr: custom_attributes
|
564
|
+
else
|
565
|
+
hash_create obj.to_h, id, attr: custom_attributes
|
566
|
+
end
|
567
|
+
|
568
|
+
@dirty_flag = true
|
569
|
+
|
570
|
+
puts 'before save ' + @autosave.inspect if @debug
|
571
|
+
save() if @autosave
|
572
|
+
|
573
|
+
self
|
574
|
+
end
|
575
|
+
|
576
|
+
#Create a record from a string, given the dynarex document contains a format mask.
|
577
|
+
# dynarex = Dynarex.new 'contacts/contact(name,age,dob)'
|
578
|
+
# dynarex.create_from_line 'Tracy 37 15-Jun-1972'
|
579
|
+
|
580
|
+
def create_from_line(line, id=nil, attr: '')
|
581
|
+
t = @format_mask.to_s.gsub(/\[!(\w+)\]/, '(.*)').sub(/\[/,'\[')\
|
582
|
+
.sub(/\]/,'\]')
|
583
|
+
line.match(/#{t}/).captures
|
584
|
+
|
585
|
+
a = line.match(/#{t}/).captures
|
586
|
+
h = Hash[@fields.zip(a)]
|
587
|
+
create h
|
588
|
+
self
|
589
|
+
end
|
590
|
+
|
591
|
+
def default_key=(id)
|
592
|
+
@default_key = id.to_sym
|
593
|
+
@summary[:default_key] = id
|
594
|
+
@fields << id.to_sym
|
595
|
+
end
|
596
|
+
|
597
|
+
|
598
|
+
#Updates a record from an id and a hash containing field name and field value.
|
599
|
+
# dynarex.update 4, name: Jeff, age: 38
|
600
|
+
|
601
|
+
def update(id, obj)
|
602
|
+
|
603
|
+
params = if obj.is_a? Hash then
|
604
|
+
obj
|
605
|
+
elsif obj.is_a? RecordX
|
606
|
+
obj.to_h
|
607
|
+
end
|
608
|
+
|
609
|
+
fields = capture_fields(params)
|
610
|
+
|
611
|
+
# for each field update each record field
|
612
|
+
record = @doc.root.element("records/#{@record_name}[@id='#{id.to_s}']")
|
613
|
+
|
614
|
+
fields.each do |k,v|
|
615
|
+
puts "updating ... %s = '%s'" % [k,v] if @debug
|
616
|
+
record.element(k.to_s).text = v if v
|
617
|
+
end
|
618
|
+
|
619
|
+
record.add_attribute(last_modified: Time.now.to_s)
|
620
|
+
|
621
|
+
@dirty_flag = true
|
622
|
+
|
623
|
+
save() if @autosave
|
624
|
+
|
625
|
+
self
|
626
|
+
|
627
|
+
end
|
628
|
+
|
629
|
+
|
630
|
+
#Delete a record.
|
631
|
+
# dyarex.delete 3 # deletes record with id 3
|
632
|
+
|
633
|
+
def delete(x)
|
634
|
+
|
635
|
+
return x.each {|id| self.delete id} if x.is_a? Array
|
636
|
+
|
637
|
+
if x.to_i.to_s == x.to_s and x[/[0-9]/] then
|
638
|
+
@doc.root.delete("records/*[@id='#{x}']")
|
639
|
+
else
|
640
|
+
@doc.delete x
|
641
|
+
end
|
642
|
+
|
643
|
+
@dirty_flag = true
|
644
|
+
save() if @autosave
|
645
|
+
|
646
|
+
self
|
647
|
+
end
|
648
|
+
|
649
|
+
def element(x)
|
650
|
+
@doc.root.element x
|
651
|
+
end
|
652
|
+
|
653
|
+
def sort_by!(field=nil, &element_blk)
|
654
|
+
|
655
|
+
blk = field ? ->(x){ x.text(field.to_s).to_s} : element_blk
|
656
|
+
r = sort_records_by! &blk
|
657
|
+
@dirty_flag = true
|
658
|
+
r
|
659
|
+
|
660
|
+
end
|
661
|
+
|
662
|
+
|
663
|
+
def record(id)
|
664
|
+
e = @doc.root.element("records/*[@id='#{id}']")
|
665
|
+
recordx_to_record e if e
|
666
|
+
end
|
667
|
+
|
668
|
+
alias find record
|
669
|
+
alias find_by_id record
|
670
|
+
|
671
|
+
def record_exists?(id)
|
672
|
+
!@doc.root.element("records/*[@id='#{id}']").nil?
|
673
|
+
end
|
674
|
+
|
675
|
+
def refresh()
|
676
|
+
@dirty_flag = true
|
677
|
+
end
|
678
|
+
|
679
|
+
def refresh!()
|
680
|
+
(load_records; rebuild_doc) if @dirty_flag == true
|
681
|
+
end
|
682
|
+
|
683
|
+
# used internally by to_rss()
|
684
|
+
#
|
685
|
+
def rss_xslt(opt={})
|
686
|
+
|
687
|
+
h = {limit: 11}.merge(opt)
|
688
|
+
doc = Rexle.new(self.to_xslt)
|
689
|
+
e = doc.element('//xsl:apply-templates[2]')
|
690
|
+
|
691
|
+
e2 = doc.root.element('xsl:template[3]')
|
692
|
+
item = e2.element('item')
|
693
|
+
new_item = item.deep_clone
|
694
|
+
item.delete
|
695
|
+
|
696
|
+
pubdate = @xslt_schema[/pubDate:/]
|
697
|
+
xslif = Rexle.new("<xsl:if test='position() < #{h[:limit]}'/>").root
|
698
|
+
|
699
|
+
if pubdate.nil? then
|
700
|
+
pubdate = Rexle.new("<pubDate><xsl:value-of select='pubDate'>" + \
|
701
|
+
"</xsl:value-of></pubDate>").root
|
702
|
+
new_item.add pubdate
|
703
|
+
end
|
704
|
+
|
705
|
+
xslif.add new_item
|
706
|
+
e2.add xslif.root
|
707
|
+
xslt = doc.xml
|
708
|
+
|
709
|
+
xslt
|
710
|
+
|
711
|
+
end
|
712
|
+
|
713
|
+
def filter(&blk)
|
714
|
+
|
715
|
+
dx = Dynarex.new @schema
|
716
|
+
self.all.select(&blk).each {|x| dx.create x}
|
717
|
+
dx
|
718
|
+
|
719
|
+
end
|
720
|
+
|
721
|
+
def to_xslt(opt={})
|
722
|
+
|
723
|
+
h = {limit: -1}.merge(opt)
|
724
|
+
@xslt_schema = @xslt_schema || self.summary[:xslt_schema]
|
725
|
+
raise 'to_xslt(): xslt_schema nil' unless @xslt_schema
|
726
|
+
|
727
|
+
xslt = DynarexXSLT.new(schema: @schema, xslt_schema: @xslt_schema ).to_xslt
|
728
|
+
|
729
|
+
return xslt
|
730
|
+
end
|
731
|
+
|
732
|
+
def to_rss(opt={}, xslt=nil)
|
733
|
+
|
734
|
+
puts 'inside to_rss'.info if @debug
|
735
|
+
|
736
|
+
unless xslt then
|
737
|
+
|
738
|
+
h = {limit: 11}.merge(opt)
|
739
|
+
doc = Rexle.new(self.to_xslt)
|
740
|
+
e = doc.element('//xsl:apply-templates[2]')
|
741
|
+
|
742
|
+
e2 = doc.root.element('xsl:template[3]')
|
743
|
+
item = e2.element('item')
|
744
|
+
new_item = item.deep_clone
|
745
|
+
item.delete
|
746
|
+
|
747
|
+
pubdate = @xslt_schema[/pubDate:/]
|
748
|
+
xslif = Rexle.new("<xsl:if test='position() < #{h[:limit]}'/>").root
|
749
|
+
|
750
|
+
|
751
|
+
if pubdate.nil? then
|
752
|
+
pubdate2 = Rexle.new("<pubDate><xsl:value-of select='pubDate'></xsl:value-of></pubDate>").root
|
753
|
+
new_item.add pubdate2
|
754
|
+
end
|
755
|
+
|
756
|
+
xslif.add new_item
|
757
|
+
e2.add xslif
|
758
|
+
xslt = doc.xml
|
759
|
+
|
760
|
+
xslt
|
761
|
+
end
|
762
|
+
|
763
|
+
doc = Rexle.new(self.to_xml)
|
764
|
+
|
765
|
+
puts ('pubdate: ' + pubdate.inspect).debug if @debug
|
766
|
+
|
767
|
+
if pubdate.nil? then
|
768
|
+
doc.root.xpath('records/*').each do |x|
|
769
|
+
raw_dt = DateTime.parse x.attributes[:created]
|
770
|
+
dt = raw_dt.strftime("%a, %d %b %Y %H:%M:%S %z")
|
771
|
+
x.add Rexle::Element.new('pubDate').add_text dt.to_s
|
772
|
+
end
|
773
|
+
end
|
774
|
+
|
775
|
+
puts ('doc: ' + doc.root.xml) if @debug
|
776
|
+
File.write '/tmp/blog.xml', doc.root.xml
|
777
|
+
puts ('xslt:' + xslt.inspect) if @debug
|
778
|
+
File.write '/tmp/blog.xslt', xslt
|
779
|
+
|
780
|
+
out = Rexslt.new(xslt, doc).to_s(declaration: false)
|
781
|
+
|
782
|
+
#Rexle.new("<rss version='2.0'>%s</rss>" % xml).xml(pretty: true)
|
783
|
+
|
784
|
+
doc = Rexle.new("<rss version='2.0'>%s</rss>" % out.to_s)
|
785
|
+
yield( doc ) if block_given?
|
786
|
+
xml = doc.xml(pretty: true)
|
787
|
+
xml
|
788
|
+
end
|
789
|
+
|
790
|
+
def unique=(bool)
|
791
|
+
self.summary.merge!({unique: bool})
|
792
|
+
@dirty_flag = true
|
793
|
+
@unique = bool
|
794
|
+
end
|
795
|
+
|
796
|
+
def xpath(x)
|
797
|
+
@doc.root.xpath x
|
798
|
+
end
|
799
|
+
|
800
|
+
def xslt=(value)
|
801
|
+
|
802
|
+
self.summary.merge!({xslt: value})
|
803
|
+
@dirty_flag = true
|
804
|
+
@xslt = value
|
805
|
+
end
|
806
|
+
|
807
|
+
private
|
808
|
+
|
809
|
+
def add_id(a)
|
810
|
+
@default_key = :uid
|
811
|
+
@summary[:default_key] = 'uid'
|
812
|
+
@fields << :uid
|
813
|
+
a.each.with_index{|x,i| x << (i+1).to_s}
|
814
|
+
end
|
815
|
+
|
816
|
+
def create_find(fields)
|
817
|
+
|
818
|
+
methods = fields.map do |field|
|
819
|
+
"def find_by_#{field}(value) findx_by('#{field}', value) end\n" + \
|
820
|
+
"def find_all_by_#{field}(value) findx_all_by(\"#{field}\", value) end"
|
821
|
+
end
|
822
|
+
self.instance_eval(methods.join("\n"))
|
823
|
+
end
|
824
|
+
|
825
|
+
def findx_by(field, value)
|
826
|
+
#@logger.debug "field: #{field.inspect}, value: #{value.inspect}"
|
827
|
+
(load_records; rebuild_doc) if @dirty_flag == true
|
828
|
+
r = @doc.root.element("records/*[#{field}=\"#{value}\"]")
|
829
|
+
r ? recordx_to_record(r) : nil
|
830
|
+
end
|
831
|
+
|
832
|
+
def findx_all_by(field, value)
|
833
|
+
@doc.root.xpath("records/*[#{field}=\"#{value}\"]")\
|
834
|
+
.map {|x| recordx_to_record x}
|
835
|
+
end
|
836
|
+
|
837
|
+
def recordx_to_record(recordx)
|
838
|
+
|
839
|
+
h = recordx.attributes
|
840
|
+
|
841
|
+
records = recordx.xpath("*").map {|x| x.text ? x.text.unescape.to_s : '' }
|
842
|
+
hash = @fields.zip(records).to_h
|
843
|
+
RecordX.new(hash, self, h[:id], h[:created], h[:last_modified])
|
844
|
+
|
845
|
+
end
|
846
|
+
|
847
|
+
def hash_create(raw_params={}, id=nil, attr: {})
|
848
|
+
|
849
|
+
puts 'inside hash_create' if @debug
|
850
|
+
record = make_record(raw_params, id, attr: attr)
|
851
|
+
puts 'record: ' + record.inspect if @debug
|
852
|
+
method_name = @order == 'ascending' ? :add : :prepend
|
853
|
+
@doc.root.element('records').method(method_name).call record
|
854
|
+
|
855
|
+
end
|
856
|
+
|
857
|
+
def capture_fields(params)
|
858
|
+
fields = Hash[@fields.map {|x| [x,nil]}]
|
859
|
+
fields.keys.each {|key| fields[key] = params[key.to_sym] if params.has_key? key.to_sym}
|
860
|
+
fields
|
861
|
+
end
|
862
|
+
|
863
|
+
def display_xml(options={})
|
864
|
+
#@logger.debug 'inside display_xml'
|
865
|
+
opt = {unescape_html: false}.merge options
|
866
|
+
|
867
|
+
state = :external
|
868
|
+
#@logger.debug 'before diry'
|
869
|
+
if @dirty_flag == true then
|
870
|
+
load_records
|
871
|
+
state = :internal
|
872
|
+
end
|
873
|
+
#@logger.debug 'before rebuilt'
|
874
|
+
doc = rebuild_doc(state)
|
875
|
+
#@logger.debug 'after rebuild_doc'
|
876
|
+
|
877
|
+
if opt[:unescape_html] == true then
|
878
|
+
doc.content(opt)
|
879
|
+
else
|
880
|
+
doc.xml(opt)
|
881
|
+
end
|
882
|
+
end
|
883
|
+
|
884
|
+
def make_record(raw_params, id=nil, attr: {})
|
885
|
+
|
886
|
+
id = (@doc.root.xpath('max(records/*/attribute::id)') || '0').succ unless id
|
887
|
+
raw_params.merge!(uid: id) if @default_key.to_sym == :uid
|
888
|
+
params = Hash[raw_params.keys.map(&:to_sym).zip(raw_params.values)]
|
889
|
+
|
890
|
+
fields = capture_fields(params)
|
891
|
+
record = Rexle::Element.new @record_name
|
892
|
+
|
893
|
+
fields.each do |k,v|
|
894
|
+
element = Rexle::Element.new(k.to_s)
|
895
|
+
element.root.text = v.to_s.gsub('<','<').gsub('>','>') if v
|
896
|
+
record.add element if record
|
897
|
+
end
|
898
|
+
|
899
|
+
attributes = {id: id.to_s, created: Time.now.to_s, last_modified: nil}\
|
900
|
+
.merge attr
|
901
|
+
attributes.each {|k,v| record.add_attribute(k, v)}
|
902
|
+
|
903
|
+
record
|
904
|
+
end
|
905
|
+
|
906
|
+
alias refresh_doc display_xml
|
907
|
+
|
908
|
+
def parse_links(raw_lines)
|
909
|
+
|
910
|
+
raw_lines.map do |line|
|
911
|
+
|
912
|
+
buffer = RXFHelper.read(line.chomp, auto: false).first
|
913
|
+
|
914
|
+
doc = Rexle.new buffer
|
915
|
+
|
916
|
+
if doc.root.name == 'kvx' then
|
917
|
+
|
918
|
+
kvx = Kvx.new doc
|
919
|
+
h = kvx.to_h[:body]
|
920
|
+
@fields.inject([]){|r,x| r << h[x]}
|
921
|
+
|
922
|
+
end
|
923
|
+
|
924
|
+
end
|
925
|
+
|
926
|
+
end
|
927
|
+
|
928
|
+
def rebuild_doc(state=:internal)
|
929
|
+
|
930
|
+
puts 'inside rebuild_doc'.info if @debug
|
931
|
+
|
932
|
+
reserved_keywords = (
|
933
|
+
Object.public_methods | \
|
934
|
+
Kernel.public_methods | \
|
935
|
+
public_methods + [:method_missing]
|
936
|
+
)
|
937
|
+
|
938
|
+
xml = RexleBuilder.new
|
939
|
+
|
940
|
+
a = xml.send @root_name do
|
941
|
+
|
942
|
+
xml.summary do
|
943
|
+
|
944
|
+
@summary.each do |key,value|
|
945
|
+
|
946
|
+
v = value.to_s.gsub('>','>')\
|
947
|
+
.gsub('<','<')\
|
948
|
+
.gsub(/(&\s|&[a-zA-Z\.]+;?)/) {|x| x[-1] == ';' ? x \
|
949
|
+
: x.sub('&','&')}
|
950
|
+
|
951
|
+
xml.send key, v
|
952
|
+
|
953
|
+
end
|
954
|
+
end
|
955
|
+
|
956
|
+
records = @records.to_a
|
957
|
+
|
958
|
+
if records then
|
959
|
+
|
960
|
+
#jr160315records.reverse! if @order == 'descending' and state == :external
|
961
|
+
|
962
|
+
xml.records do
|
963
|
+
|
964
|
+
records.each do |k, item|
|
965
|
+
|
966
|
+
attributes = {}
|
967
|
+
|
968
|
+
item.keys.each do |key|
|
969
|
+
attributes[key] = item[key] || '' unless key == :body
|
970
|
+
end
|
971
|
+
|
972
|
+
if @record_name.nil? then
|
973
|
+
raise DynarexException, 'record_name can\'t be nil. Check the schema'
|
974
|
+
end
|
975
|
+
|
976
|
+
xml.send(@record_name, attributes) do
|
977
|
+
item[:body].each do |name,value|
|
978
|
+
|
979
|
+
if reserved_keywords.include? name then
|
980
|
+
name = ('._' + name.to_s).to_sym
|
981
|
+
end
|
982
|
+
|
983
|
+
val = value.send(value.is_a?(String) ? :to_s : :to_yaml)
|
984
|
+
xml.send(name, val.gsub('>','>')\
|
985
|
+
.gsub('<','<')\
|
986
|
+
.gsub(/(&\s|&[a-zA-Z\.]+;?)/) do |x|
|
987
|
+
x[-1] == ';' ? x : x.sub('&','&')
|
988
|
+
end
|
989
|
+
)
|
990
|
+
end
|
991
|
+
end
|
992
|
+
end
|
993
|
+
|
994
|
+
end
|
995
|
+
else
|
996
|
+
xml.records
|
997
|
+
end # end of if @records
|
998
|
+
end
|
999
|
+
|
1000
|
+
doc = Rexle.new(a)
|
1001
|
+
|
1002
|
+
puts ('@xslt: ' + @xslt.inspect).debug if @debug
|
1003
|
+
|
1004
|
+
if @xslt then
|
1005
|
+
doc.instructions = [['xml-stylesheet',
|
1006
|
+
"title='XSL_formatting' type='text/xsl' href='#{@xslt}'"]]
|
1007
|
+
end
|
1008
|
+
|
1009
|
+
return doc if state != :internal
|
1010
|
+
@doc = doc
|
1011
|
+
end
|
1012
|
+
|
1013
|
+
def string_parse(buffer)
|
1014
|
+
|
1015
|
+
if @spaces_delimited then
|
1016
|
+
buffer = buffer.lines.map{|x| x.gsub(/\s{2,}/,' # ')}.join
|
1017
|
+
end
|
1018
|
+
|
1019
|
+
buffer.gsub!("\r",'')
|
1020
|
+
buffer.gsub!(/\n-{4,}\n/,"\n\n")
|
1021
|
+
buffer.gsub!(/---\n/m, "--- ")
|
1022
|
+
|
1023
|
+
buffer.gsub!(/.>/) {|x| x[0] != '?' ? x.sub(/>/,'>') : x }
|
1024
|
+
buffer.gsub!(/<./) {|x| x[1] != '?' ? x.sub(/</,'<') : x }
|
1025
|
+
|
1026
|
+
@raw_header = buffer[/<\?dynarex[^>]+>/]
|
1027
|
+
|
1028
|
+
if buffer[/<\?/] then
|
1029
|
+
|
1030
|
+
raw_stylesheet = buffer.slice!(/<\?xml-stylesheet[^>]+>/)
|
1031
|
+
@xslt = raw_stylesheet[/href=["']([^"']+)/,1] if raw_stylesheet
|
1032
|
+
@raw_header = buffer.slice!(/<\?dynarex[^>]+>/) + "\n"
|
1033
|
+
|
1034
|
+
header = @raw_header[/<?dynarex (.*)?>/,1]
|
1035
|
+
|
1036
|
+
r1 = /([\w\-]+\s*\=\s*'[^']*)'/
|
1037
|
+
r2 = /([\w\-]+\s*\=\s*"[^"]*)"/
|
1038
|
+
|
1039
|
+
r = header.scan(/#{r1}|#{r2}/).map(&:compact).flatten
|
1040
|
+
|
1041
|
+
r.each do |x|
|
1042
|
+
|
1043
|
+
attr, val = x.split(/\s*=\s*["']/,2)
|
1044
|
+
name = (attr + '=').to_sym
|
1045
|
+
|
1046
|
+
if self.public_methods.include? name then
|
1047
|
+
self.method(name).call(unescape val)
|
1048
|
+
else
|
1049
|
+
puts "Dynarex: warning: method %s doesn't exist." % [name.inspect]
|
1050
|
+
end
|
1051
|
+
end
|
1052
|
+
|
1053
|
+
end
|
1054
|
+
|
1055
|
+
# if records already exist find the max id
|
1056
|
+
i = @doc.root.xpath('max(records/*/attribute::id)').to_i
|
1057
|
+
|
1058
|
+
raw_summary = schema[/\[([^\]]+)/,1]
|
1059
|
+
|
1060
|
+
raw_lines = buffer.lines.to_a
|
1061
|
+
|
1062
|
+
if raw_summary then
|
1063
|
+
|
1064
|
+
a_summary = raw_summary.split(',').map(&:strip)
|
1065
|
+
|
1066
|
+
@summary ||= {}
|
1067
|
+
raw_lines.shift while raw_lines.first.strip.empty?
|
1068
|
+
|
1069
|
+
# fetch any summary lines
|
1070
|
+
while not raw_lines.empty? and \
|
1071
|
+
raw_lines.first[/#{a_summary.join('|')}:\s+\S+/] do
|
1072
|
+
|
1073
|
+
label, val = raw_lines.shift.chomp.match(/(\w+):\s*([^$]+)$/).captures
|
1074
|
+
@summary[label.to_sym] = val
|
1075
|
+
end
|
1076
|
+
|
1077
|
+
self.xslt = @summary[:xslt] || @summary[:xsl] if @summary[:xslt]\
|
1078
|
+
or @summary[:xsl]
|
1079
|
+
end
|
1080
|
+
|
1081
|
+
@summary[:recordx_type] = 'dynarex'
|
1082
|
+
@summary[:schema] = @schema
|
1083
|
+
@summary[:format_mask] = @format_mask
|
1084
|
+
@summary[:unique] = @unique if @unique
|
1085
|
+
|
1086
|
+
raw_lines.shift while raw_lines.first.strip.empty?
|
1087
|
+
|
1088
|
+
lines = case raw_lines.first.rstrip
|
1089
|
+
|
1090
|
+
when '---'
|
1091
|
+
|
1092
|
+
yaml = YAML.load raw_lines.join("\n")
|
1093
|
+
|
1094
|
+
yamlize = lambda {|x| (x.is_a? Array) ? x.to_yaml : x}
|
1095
|
+
|
1096
|
+
yprocs = {
|
1097
|
+
Hash: lambda {|y|
|
1098
|
+
y.map do |k,v|
|
1099
|
+
procs = {Hash: proc {|x| x.values}, Array: proc {v}}
|
1100
|
+
values = procs[v.class.to_s.to_sym].call(v).map(&yamlize)
|
1101
|
+
[k, *values]
|
1102
|
+
end
|
1103
|
+
},
|
1104
|
+
Array: lambda {|y| y.map {|x2| x2.map(&yamlize)} }
|
1105
|
+
}
|
1106
|
+
|
1107
|
+
yprocs[yaml.class.to_s.to_sym].call yaml
|
1108
|
+
|
1109
|
+
when '--+'
|
1110
|
+
|
1111
|
+
rowx(raw_lines)
|
1112
|
+
|
1113
|
+
when '--#'
|
1114
|
+
|
1115
|
+
self.summary[:rawdoc_type] = 'sectionx'
|
1116
|
+
raw_lines.shift
|
1117
|
+
|
1118
|
+
raw_lines.join.lstrip.split(/(?=^#[^#])/).map {|x| [x.rstrip]}
|
1119
|
+
|
1120
|
+
else
|
1121
|
+
|
1122
|
+
raw_lines = raw_lines.join("\n").gsub(/^(\s*#[^\n]+|\n)/,'').lines.to_a
|
1123
|
+
|
1124
|
+
if @linked then
|
1125
|
+
|
1126
|
+
parse_links(raw_lines)
|
1127
|
+
|
1128
|
+
else
|
1129
|
+
a2 = raw_lines.map.with_index do |x,i|
|
1130
|
+
|
1131
|
+
next if x[/^\s+$|\n\s*#/]
|
1132
|
+
|
1133
|
+
begin
|
1134
|
+
|
1135
|
+
field_names, field_values = RXRawLineParser.new(@format_mask).parse(x)
|
1136
|
+
rescue
|
1137
|
+
raise "input file parser error at line " + (i + 1).to_s + ' --> ' + x
|
1138
|
+
end
|
1139
|
+
field_values
|
1140
|
+
end
|
1141
|
+
|
1142
|
+
a2.compact!
|
1143
|
+
a3 = a2.compact.map(&:first)
|
1144
|
+
|
1145
|
+
if a3 != a3.uniq then
|
1146
|
+
|
1147
|
+
if @unique then
|
1148
|
+
raise DynarexException, "Duplicate id found"
|
1149
|
+
else
|
1150
|
+
add_id(a2)
|
1151
|
+
end
|
1152
|
+
|
1153
|
+
end
|
1154
|
+
|
1155
|
+
a2
|
1156
|
+
end
|
1157
|
+
|
1158
|
+
end
|
1159
|
+
|
1160
|
+
a = lines.map.with_index do |x,i|
|
1161
|
+
|
1162
|
+
created = Time.now.to_s
|
1163
|
+
|
1164
|
+
h = Hash[
|
1165
|
+
@fields.zip(
|
1166
|
+
x.map do |t|
|
1167
|
+
|
1168
|
+
t.to_s[/^---(?:\s|\n)/] ? YAML.load(t[/^---(?:\s|\n)(.*)/,1]) : unescape(t.to_s)
|
1169
|
+
end
|
1170
|
+
)
|
1171
|
+
]
|
1172
|
+
h[@fields.last] = checked[i].to_s if @type == 'checklist'
|
1173
|
+
[h[@default_key], {id: '', created: created, last_modified: '', body: h}]
|
1174
|
+
end
|
1175
|
+
|
1176
|
+
h2 = Hash[a]
|
1177
|
+
|
1178
|
+
#replace the existing records hash
|
1179
|
+
h = @records
|
1180
|
+
i = 0
|
1181
|
+
h2.each do |key,item|
|
1182
|
+
if h.has_key? key then
|
1183
|
+
|
1184
|
+
# overwrite the previous item and change the timestamps
|
1185
|
+
h[key][:last_modified] = item[:created]
|
1186
|
+
item[:body].each do |k,v|
|
1187
|
+
h[key][:body][k.to_sym] = v
|
1188
|
+
end
|
1189
|
+
else
|
1190
|
+
item[:id] = (@order == 'descending' ? (h2.count) - i : i+ 1).to_s
|
1191
|
+
i += 1
|
1192
|
+
h[key] = item.clone
|
1193
|
+
end
|
1194
|
+
end
|
1195
|
+
|
1196
|
+
h.each {|key, item| h.delete(key) if not h2.has_key? key}
|
1197
|
+
|
1198
|
+
@flat_records = @records.values.map{|x| x[:body]}
|
1199
|
+
|
1200
|
+
rebuild_doc
|
1201
|
+
self
|
1202
|
+
end
|
1203
|
+
|
1204
|
+
def sort_records_by!(&element_blk)
|
1205
|
+
|
1206
|
+
refresh_doc
|
1207
|
+
a = @doc.root.xpath('records/*').sort_by &element_blk
|
1208
|
+
@doc.root.delete('records')
|
1209
|
+
|
1210
|
+
records = Rexle::Element.new 'records'
|
1211
|
+
|
1212
|
+
a.each {|record| records.add record}
|
1213
|
+
|
1214
|
+
@doc.root.add records
|
1215
|
+
|
1216
|
+
load_records if @dirty_flag
|
1217
|
+
self
|
1218
|
+
end
|
1219
|
+
|
1220
|
+
def unescape(s)
|
1221
|
+
s.gsub('<', '<').gsub('>','>')
|
1222
|
+
end
|
1223
|
+
|
1224
|
+
def dynarex_new(s, default_key: nil)
|
1225
|
+
|
1226
|
+
@schema = schema = s
|
1227
|
+
@default_key = default_key if default_key
|
1228
|
+
|
1229
|
+
ptrn = %r((\w+)\[?([^\]]+)?\]?\/(\w+)\(([^\)]+)\))
|
1230
|
+
|
1231
|
+
if s.match(ptrn) then
|
1232
|
+
|
1233
|
+
@root_name, raw_summary, record_name, raw_fields = s.match(ptrn).captures
|
1234
|
+
summary, fields = [raw_summary || '',raw_fields].map {|x| x.split(/,/).map &:strip}
|
1235
|
+
|
1236
|
+
if fields.include? 'id' then
|
1237
|
+
raise 'Dynarex#dynarex_new: schema field id is a reserved keyword'
|
1238
|
+
end
|
1239
|
+
|
1240
|
+
create_find fields
|
1241
|
+
|
1242
|
+
reserved = %w(require parent)
|
1243
|
+
raise 'reserved keyword' if (fields & reserved).any?
|
1244
|
+
|
1245
|
+
else
|
1246
|
+
ptrn = %r((\w+)\[?([^\]]+)?\]?)
|
1247
|
+
@root_name, raw_summary = s.match(ptrn).captures
|
1248
|
+
summary = raw_summary.split(/,/).map &:strip
|
1249
|
+
|
1250
|
+
end
|
1251
|
+
|
1252
|
+
format_mask = fields ? fields.map {|x| "[!%s]" % x}.join(' ') : ''
|
1253
|
+
|
1254
|
+
@summary = Hash[summary.zip([''] * summary.length).flatten.each_slice(2)\
|
1255
|
+
.map{|x1,x2| [x1.to_sym,x2]}]
|
1256
|
+
@summary.merge!({recordx_type: 'dynarex', format_mask: format_mask, schema: s})
|
1257
|
+
@records = {}
|
1258
|
+
@flat_records = {}
|
1259
|
+
|
1260
|
+
rebuild_doc
|
1261
|
+
|
1262
|
+
end
|
1263
|
+
|
1264
|
+
def attach_record_methods()
|
1265
|
+
create_find @fields
|
1266
|
+
end
|
1267
|
+
|
1268
|
+
def openx(s)
|
1269
|
+
#@logger.debug 'inside openx'
|
1270
|
+
if s[/</] then # xml
|
1271
|
+
#@logger.debug 'regular string'
|
1272
|
+
#@logger.debug 's: ' + s.inspect
|
1273
|
+
buffer = s
|
1274
|
+
|
1275
|
+
elsif s[/[\[\(]/] # schema
|
1276
|
+
|
1277
|
+
dynarex_new(s)
|
1278
|
+
|
1279
|
+
elsif s[/^https?:\/\//] then # url
|
1280
|
+
buffer, type = RXFHelper.read s, {username: @username,
|
1281
|
+
password: @password, auto: false}
|
1282
|
+
elsif s[/^dfs?:\/\//] then
|
1283
|
+
|
1284
|
+
@local_filepath = s
|
1285
|
+
|
1286
|
+
if FileX.exists? s then
|
1287
|
+
buffer = FileX.read s
|
1288
|
+
elsif @schema
|
1289
|
+
dynarex_new @schema, default_key: @default_key
|
1290
|
+
end
|
1291
|
+
|
1292
|
+
else # local file
|
1293
|
+
|
1294
|
+
@local_filepath = s
|
1295
|
+
|
1296
|
+
if File.exists? s then
|
1297
|
+
buffer = File.read s
|
1298
|
+
elsif @schema
|
1299
|
+
dynarex_new @schema, default_key: @default_key
|
1300
|
+
else
|
1301
|
+
raise DynarexException, 'file not found: ' + s
|
1302
|
+
end
|
1303
|
+
end
|
1304
|
+
#@logger.debug 'buffer: ' + buffer[0..120]
|
1305
|
+
|
1306
|
+
return import(buffer) if buffer =~ /^<\?dynarex\b/
|
1307
|
+
|
1308
|
+
if buffer then
|
1309
|
+
|
1310
|
+
raw_stylesheet = buffer.slice!(/<\?xml-stylesheet[^>]+>/)
|
1311
|
+
@xslt = raw_stylesheet[/href=["']([^"']+)/,1] if raw_stylesheet
|
1312
|
+
|
1313
|
+
@doc = Rexle.new(buffer) unless @doc
|
1314
|
+
#@logger.debug 'openx/@doc : ' + @doc.xml.inspect
|
1315
|
+
end
|
1316
|
+
|
1317
|
+
return if @doc.root.nil?
|
1318
|
+
e = @doc.root.element('summary')
|
1319
|
+
|
1320
|
+
@schema = e.text('schema')
|
1321
|
+
@root_name = @doc.root.name
|
1322
|
+
@summary = summary_to_h
|
1323
|
+
|
1324
|
+
summary_methods = (@summary.keys - self.public_methods)
|
1325
|
+
|
1326
|
+
summary_methods.each do |x|
|
1327
|
+
|
1328
|
+
instance_eval "
|
1329
|
+
|
1330
|
+
def #{x.to_sym}()
|
1331
|
+
@summary[:#{x}]
|
1332
|
+
end
|
1333
|
+
|
1334
|
+
def #{x.to_s}=(v)
|
1335
|
+
@summary[:#{x}] = v
|
1336
|
+
@doc.root.element('summary/#{x.to_s}').text = v
|
1337
|
+
end
|
1338
|
+
"
|
1339
|
+
end
|
1340
|
+
|
1341
|
+
@order = @summary[:order] if @summary.has_key? :order
|
1342
|
+
|
1343
|
+
|
1344
|
+
@default_key ||= e.text('default_key')
|
1345
|
+
@format_mask = e.text('format_mask')
|
1346
|
+
@xslt = e.text('xslt')
|
1347
|
+
|
1348
|
+
@fields = @schema[/([^(]+)\)$/,1].split(/\s*,\s*/).map(&:to_sym)
|
1349
|
+
|
1350
|
+
@fields << @default_key if @default_key and not @default_key.empty? and \
|
1351
|
+
!@fields.include? @default_key.to_sym
|
1352
|
+
|
1353
|
+
if @schema and @schema.match(/(\w+)\(([^\)]+)/) then
|
1354
|
+
@record_name, raw_fields = @schema.match(/(\w+)\(([^\)]+)/).captures
|
1355
|
+
@fields = raw_fields.split(',').map{|x| x.strip.to_sym} unless @fields
|
1356
|
+
end
|
1357
|
+
|
1358
|
+
if @fields then
|
1359
|
+
|
1360
|
+
@default_key = @fields[0] unless @default_key
|
1361
|
+
# load the record query handler methods
|
1362
|
+
attach_record_methods
|
1363
|
+
else
|
1364
|
+
|
1365
|
+
#jr080912 @default_key = @doc.root.xpath('records/*/*').first.name
|
1366
|
+
@default_key = @doc.root.element('records/./.[1]').name
|
1367
|
+
end
|
1368
|
+
|
1369
|
+
@summary[:default_key] = @default_key.to_s
|
1370
|
+
|
1371
|
+
if @doc.root.xpath('records/*').length > 0 then
|
1372
|
+
@record_name = @doc.root.element('records/*[1]').name
|
1373
|
+
#jr240913 load_records
|
1374
|
+
@dirty_flag = true
|
1375
|
+
end
|
1376
|
+
|
1377
|
+
end
|
1378
|
+
|
1379
|
+
def load_records
|
1380
|
+
|
1381
|
+
puts 'inside load_records'.info if @debug
|
1382
|
+
|
1383
|
+
@dirty_flag = false
|
1384
|
+
|
1385
|
+
if @summary[:order] then
|
1386
|
+
orderfield = @summary[:order][/(\w+)\s+(?:ascending|descending)/,1]
|
1387
|
+
sort_records_by! {|x| x.element(orderfield).text } if orderfield
|
1388
|
+
end
|
1389
|
+
|
1390
|
+
@records = records_to_h
|
1391
|
+
|
1392
|
+
@records.instance_eval do
|
1393
|
+
def delete_item(i)
|
1394
|
+
self.delete self.keys[i]
|
1395
|
+
end
|
1396
|
+
end
|
1397
|
+
|
1398
|
+
#Returns a ready-only snapshot of records as a simple Hash.
|
1399
|
+
@flat_records = @records.values.map{|x| x[:body]}
|
1400
|
+
|
1401
|
+
end
|
1402
|
+
|
1403
|
+
|
1404
|
+
def display()
|
1405
|
+
puts @doc.to_s
|
1406
|
+
end
|
1407
|
+
|
1408
|
+
def records_to_h(order=:ascending)
|
1409
|
+
|
1410
|
+
i = @doc.root.xpath('max(records/*/attribute::id)') || 0
|
1411
|
+
records = @doc.root.xpath('records/*')
|
1412
|
+
#@logger.debug 'records: ' + records.inspect
|
1413
|
+
records = records.take @limit if @limit
|
1414
|
+
|
1415
|
+
recs = records #jr160315 (order == :descending ? records.reverse : records)
|
1416
|
+
a = recs.inject({}) do |result,row|
|
1417
|
+
|
1418
|
+
created = Time.now.to_s
|
1419
|
+
last_modified = ''
|
1420
|
+
|
1421
|
+
if row.attributes[:id] then
|
1422
|
+
id = row.attributes[:id]
|
1423
|
+
else
|
1424
|
+
i += 1; id = i.to_s
|
1425
|
+
end
|
1426
|
+
|
1427
|
+
body = (@fields - ['uid']).inject({}) do |r,field|
|
1428
|
+
|
1429
|
+
node = row.element field.to_s
|
1430
|
+
|
1431
|
+
if node then
|
1432
|
+
text = node.text ? node.text.unescape : ''
|
1433
|
+
|
1434
|
+
r.merge node.name.to_sym => (text[/^---(?:\s|\n)/] ?
|
1435
|
+
YAML.load(text[/^---(?:\s|\n)(.*)/,1]) : text)
|
1436
|
+
else
|
1437
|
+
r
|
1438
|
+
end
|
1439
|
+
end
|
1440
|
+
|
1441
|
+
body[:uid] = id if @default_key == 'uid'
|
1442
|
+
|
1443
|
+
attributes = row.attributes
|
1444
|
+
result.merge body[@default_key.to_sym] => attributes.merge({id: id, body: body})
|
1445
|
+
end
|
1446
|
+
|
1447
|
+
puts 'records_to_h a: ' + a.inspect if @debug
|
1448
|
+
#@logger.debug 'a: ' + a.inspect
|
1449
|
+
a
|
1450
|
+
|
1451
|
+
end
|
1452
|
+
|
1453
|
+
def rowx(raw_lines)
|
1454
|
+
|
1455
|
+
self.summary[:rawdoc_type] = 'rowx'
|
1456
|
+
raw_lines.shift
|
1457
|
+
|
1458
|
+
a3 = raw_lines.join.strip.split(/\n\n(?=\w+:)/)
|
1459
|
+
|
1460
|
+
# get the fields
|
1461
|
+
a4 = a3.map{|x| x.scan(/^\w+(?=:)/)}.flatten(1).uniq
|
1462
|
+
|
1463
|
+
abbrv_fields = a4.all? {|x| x.length == 1}
|
1464
|
+
|
1465
|
+
a5 = a3.map do |xlines|
|
1466
|
+
|
1467
|
+
puts 'xlines: ' + xlines.inspect if @debug
|
1468
|
+
|
1469
|
+
missing_fields = a4 - xlines.scan(/^\w+(?=:)/)
|
1470
|
+
|
1471
|
+
r = xlines.split(/\n(\w+:.*)/m)
|
1472
|
+
puts 'r: ' + r.inspect if @debug
|
1473
|
+
|
1474
|
+
missing_fields.map!{|x| x + ":"}
|
1475
|
+
key = (abbrv_fields ? @fields[0].to_s[0] : @fields.first.to_s) + ':'
|
1476
|
+
|
1477
|
+
if missing_fields.include? key
|
1478
|
+
r.unshift key
|
1479
|
+
missing_fields.delete key
|
1480
|
+
end
|
1481
|
+
|
1482
|
+
r += missing_fields
|
1483
|
+
r.join("\n")
|
1484
|
+
|
1485
|
+
end
|
1486
|
+
puts 'a5: ' + a5.inspect if @debug
|
1487
|
+
|
1488
|
+
xml = RowX.new(a5.join("\n").strip, level: 0).to_xml
|
1489
|
+
puts 'xml: ' + xml.inspect if @debug
|
1490
|
+
|
1491
|
+
a2 = Rexle.new(xml).root.xpath('item').inject([]) do |r,x|
|
1492
|
+
|
1493
|
+
r << @fields.map do |field|
|
1494
|
+
x.text(abbrv_fields ? field.to_s.chr : field.to_s )
|
1495
|
+
end
|
1496
|
+
|
1497
|
+
end
|
1498
|
+
|
1499
|
+
a2.compact!
|
1500
|
+
|
1501
|
+
# if there is no field value for the first field then
|
1502
|
+
# the default_key is invalid. The default_key is changed to an ID.
|
1503
|
+
if a2.detect {|x| x.first == ''} then
|
1504
|
+
add_id(a2)
|
1505
|
+
else
|
1506
|
+
|
1507
|
+
a3 = a2.map(&:first)
|
1508
|
+
add_id(a2) if a3 != a3.uniq
|
1509
|
+
|
1510
|
+
end
|
1511
|
+
|
1512
|
+
a2
|
1513
|
+
|
1514
|
+
end
|
1515
|
+
|
1516
|
+
def sort_records
|
1517
|
+
xsl =<<XSL
|
1518
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
1519
|
+
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
|
1520
|
+
|
1521
|
+
<xsl:template match="*">
|
1522
|
+
<xsl:element name="{name()}"><xsl:text>
|
1523
|
+
</xsl:text>
|
1524
|
+
<xsl:copy-of select="summary"/><xsl:text>
|
1525
|
+
</xsl:text>
|
1526
|
+
<xsl:apply-templates select="records"/>
|
1527
|
+
</xsl:element>
|
1528
|
+
</xsl:template>
|
1529
|
+
<xsl:template match="records">
|
1530
|
+
<records><xsl:text>
|
1531
|
+
</xsl:text>
|
1532
|
+
<xsl:for-each select="child::*">
|
1533
|
+
<xsl:sort order="descending"/>
|
1534
|
+
<xsl:text> </xsl:text><xsl:copy-of select="."/><xsl:text>
|
1535
|
+
</xsl:text>
|
1536
|
+
</xsl:for-each>
|
1537
|
+
</records><xsl:text>
|
1538
|
+
</xsl:text>
|
1539
|
+
</xsl:template>
|
1540
|
+
|
1541
|
+
</xsl:stylesheet>
|
1542
|
+
XSL
|
1543
|
+
|
1544
|
+
@doc = Rexle.new(Rexslt.new(xsl, self.to_xml).to_s)
|
1545
|
+
@dirty_flag = true
|
1546
|
+
end
|
1547
|
+
|
1548
|
+
def summary_to_h
|
1549
|
+
|
1550
|
+
h = {recordx_type: 'dynarex'}
|
1551
|
+
|
1552
|
+
@doc.root.xpath('summary/*').inject(h) do |r,node|
|
1553
|
+
r.merge node.name.to_s.to_sym =>
|
1554
|
+
node.text ? node.text.unescape : node.text.to_s
|
1555
|
+
end
|
1556
|
+
end
|
1557
|
+
|
1558
|
+
end
|