structured_data_to_sql 0.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 (35) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +9 -0
  3. data/bin/structured-data-to-sql +6 -0
  4. data/lib/structured_data_to_sql/cli.rb +405 -0
  5. data/lib/structured_data_to_sql/conversion_result.rb +94 -0
  6. data/lib/structured_data_to_sql/diagnostic.rb +82 -0
  7. data/lib/structured_data_to_sql/diagnostics_report.rb +187 -0
  8. data/lib/structured_data_to_sql/errors.rb +48 -0
  9. data/lib/structured_data_to_sql/format.rb +41 -0
  10. data/lib/structured_data_to_sql/io_support.rb +154 -0
  11. data/lib/structured_data_to_sql/json/exporter_manifest.rb +127 -0
  12. data/lib/structured_data_to_sql/json/json_schema_loader.rb +310 -0
  13. data/lib/structured_data_to_sql/json/profiles/khoros_api_export.rb +1960 -0
  14. data/lib/structured_data_to_sql/json/profiles.rb +14 -0
  15. data/lib/structured_data_to_sql/json/record_streamer.rb +477 -0
  16. data/lib/structured_data_to_sql/json/schema_inferrer.rb +150 -0
  17. data/lib/structured_data_to_sql/json/shredder.rb +241 -0
  18. data/lib/structured_data_to_sql/json/sql_emitter.rb +198 -0
  19. data/lib/structured_data_to_sql/json_converter.rb +913 -0
  20. data/lib/structured_data_to_sql/mysql_dump_xml/invalid_character_report.rb +82 -0
  21. data/lib/structured_data_to_sql/mysql_dump_xml/sanitizer.rb +152 -0
  22. data/lib/structured_data_to_sql/mysql_dump_xml/sax_parser.rb +111 -0
  23. data/lib/structured_data_to_sql/mysql_dump_xml/sql_emitter.rb +104 -0
  24. data/lib/structured_data_to_sql/mysql_dump_xml/table_data_filter.rb +343 -0
  25. data/lib/structured_data_to_sql/mysql_dump_xml/table_discovery.rb +651 -0
  26. data/lib/structured_data_to_sql/mysql_dump_xml/table_structure.rb +98 -0
  27. data/lib/structured_data_to_sql/mysql_dump_xml_converter.rb +4 -0
  28. data/lib/structured_data_to_sql/options.rb +89 -0
  29. data/lib/structured_data_to_sql/progress_reporter.rb +348 -0
  30. data/lib/structured_data_to_sql/sql_text.rb +29 -0
  31. data/lib/structured_data_to_sql/version.rb +5 -0
  32. data/lib/structured_data_to_sql/xml_converter.rb +651 -0
  33. data/lib/structured_data_to_sql/xml_dump_converter.rb +4 -0
  34. data/lib/structured_data_to_sql.rb +54 -0
  35. metadata +120 -0
@@ -0,0 +1,343 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../errors"
4
+
5
+ module StructuredDataToSql
6
+ module MysqlDumpXml
7
+ class TableDataFilter
8
+ XML_CHUNK_SIZE = 64 * 1024
9
+ CDATA_START = "<![CDATA[".b
10
+ CDATA_END = "]]>".b
11
+ COMMENT_START = "<!--".b
12
+ COMMENT_END = "-->".b
13
+ PROCESSING_START = "<?".b
14
+ PROCESSING_END = "?>".b
15
+ TABLE_DATA_START = /<\s*table_data\b/n
16
+ TABLE_DATA_END = %r{<\s*/\s*table_data\b}n
17
+ ATTR = /\b([A-Za-z_:][\w:.-]*)\s*=\s*(['"])(.*?)\2/mn
18
+ SCAN_TAIL_BYTES = 128
19
+
20
+ def initialize(
21
+ path:,
22
+ table_included:,
23
+ event_handler:,
24
+ skipped_byte_handler:
25
+ )
26
+ @path = path
27
+ @table_included = table_included
28
+ @event_handler = event_handler
29
+ @skipped_byte_handler = skipped_byte_handler
30
+ @buffer = +"".b
31
+ @passthrough_until = nil
32
+ @skip_until = nil
33
+ @skipping_table = nil
34
+ end
35
+
36
+ def feed(chunk, &block)
37
+ @buffer << chunk.to_s.b
38
+ drain(final: false, &block)
39
+ end
40
+
41
+ def finish(&block)
42
+ drain(final: true, &block)
43
+ return unless @skipping_table
44
+
45
+ raise UsageError,
46
+ "Missing closing </table_data> for excluded XML table #{@skipping_table.inspect} in #{@path}. " \
47
+ "The converter skipped that table body but could not find its end; re-export the XML or include the table to inspect the malformed block."
48
+ end
49
+
50
+ private
51
+
52
+ def drain(final:, &block)
53
+ loop do
54
+ progressed =
55
+ if @skipping_table
56
+ drain_excluded_table_body(final: final, &block)
57
+ elsif @passthrough_until
58
+ drain_passthrough_until(final: final, &block)
59
+ else
60
+ drain_normal(final: final, &block)
61
+ end
62
+ break unless progressed
63
+ end
64
+ end
65
+
66
+ def drain_normal(final:)
67
+ marker = next_normal_marker
68
+ unless marker
69
+ emit_safe_prefix(final: final) { |chunk| yield chunk }
70
+ return false
71
+ end
72
+
73
+ index, type = marker
74
+ if type == :table_data
75
+ if index.positive?
76
+ emit(@buffer.slice!(0, index)) { |chunk| yield chunk }
77
+ end
78
+ tag_end = start_tag_end(0)
79
+ unless tag_end
80
+ trim_oversized_partial_tag
81
+ return false
82
+ end
83
+
84
+ tag = @buffer.slice!(0, tag_end + 1)
85
+ table = decoded_attribute(parse_attributes(tag)["name"])
86
+ included = @table_included.call(table)
87
+ @event_handler.call(:table_data_start, table, included)
88
+ emit(tag) { |chunk| yield chunk }
89
+ if included || self_closing_tag?(tag)
90
+ if self_closing_tag?(tag)
91
+ @event_handler.call(:table_data_end, table, included)
92
+ end
93
+ else
94
+ @skipping_table = table
95
+ end
96
+ true
97
+ else
98
+ start, ending = skip_markers(type)
99
+ emit(@buffer.slice!(0, index + start.bytesize)) do |chunk|
100
+ yield chunk
101
+ end
102
+ @passthrough_until = ending
103
+ true
104
+ end
105
+ end
106
+
107
+ def drain_passthrough_until(final:)
108
+ index = @buffer.index(@passthrough_until)
109
+ if index
110
+ emit(
111
+ @buffer.slice!(0, index + @passthrough_until.bytesize)
112
+ ) { |chunk| yield chunk }
113
+ @passthrough_until = nil
114
+ true
115
+ elsif final
116
+ emit(@buffer.slice!(0, @buffer.bytesize)) { |chunk| yield chunk }
117
+ @passthrough_until = nil
118
+ false
119
+ else
120
+ keep = [SCAN_TAIL_BYTES, @passthrough_until.bytesize - 1].max
121
+ length = [@buffer.bytesize - keep, 0].max
122
+ if length.positive?
123
+ emit(@buffer.slice!(0, length)) { |chunk| yield chunk }
124
+ end
125
+ false
126
+ end
127
+ end
128
+
129
+ def drain_excluded_table_body(final:)
130
+ return drain_skipped_until(final: final) if @skip_until
131
+
132
+ marker = next_excluded_marker
133
+ unless marker
134
+ discard_safe_prefix(final: final)
135
+ return false
136
+ end
137
+
138
+ index, type = marker
139
+ if type == :table_data_end
140
+ if (tag_start = enclosing_tag_start(index))
141
+ tag_end = start_tag_end(tag_start)
142
+ unless tag_end
143
+ discard(@buffer.slice!(0, tag_start)) if tag_start.positive?
144
+ trim_oversized_partial_tag
145
+ return false
146
+ end
147
+
148
+ discard(@buffer.slice!(0, tag_end + 1))
149
+ return true
150
+ end
151
+
152
+ tag_end = start_tag_end(index)
153
+ unless tag_end
154
+ discard(@buffer.slice!(0, index)) if index.positive?
155
+ trim_oversized_partial_tag
156
+ return false
157
+ end
158
+
159
+ discard(@buffer.slice!(0, index)) if index.positive?
160
+ table = @skipping_table
161
+ emit(@buffer.slice!(0, tag_end - index + 1)) { |chunk| yield chunk }
162
+ @skipping_table = nil
163
+ @event_handler.call(:table_data_end, table, false)
164
+ true
165
+ else
166
+ start, ending = skip_markers(type)
167
+ discard(@buffer.slice!(0, index + start.bytesize))
168
+ @skip_until = ending
169
+ true
170
+ end
171
+ end
172
+
173
+ def drain_skipped_until(final:)
174
+ index = @buffer.index(@skip_until)
175
+ if index
176
+ discard(@buffer.slice!(0, index + @skip_until.bytesize))
177
+ @skip_until = nil
178
+ true
179
+ elsif final
180
+ discard(@buffer.slice!(0, @buffer.bytesize))
181
+ false
182
+ else
183
+ keep = [SCAN_TAIL_BYTES, @skip_until.bytesize - 1].max
184
+ length = [@buffer.bytesize - keep, 0].max
185
+ discard(@buffer.slice!(0, length)) if length.positive?
186
+ false
187
+ end
188
+ end
189
+
190
+ def next_normal_marker
191
+ candidates = []
192
+ table_data = @buffer.index(TABLE_DATA_START)
193
+ candidates << [table_data, :table_data] if table_data
194
+ cdata = @buffer.index(CDATA_START)
195
+ candidates << [cdata, :cdata] if cdata
196
+ comment = @buffer.index(COMMENT_START)
197
+ candidates << [comment, :comment] if comment
198
+ processing = @buffer.index(PROCESSING_START)
199
+ candidates << [processing, :processing] if processing
200
+ candidates.min_by(&:first)
201
+ end
202
+
203
+ def next_excluded_marker
204
+ candidates = []
205
+ table_data_end = @buffer.index(TABLE_DATA_END)
206
+ candidates << [table_data_end, :table_data_end] if table_data_end
207
+ cdata = @buffer.index(CDATA_START)
208
+ candidates << [cdata, :cdata] if cdata
209
+ comment = @buffer.index(COMMENT_START)
210
+ candidates << [comment, :comment] if comment
211
+ processing = @buffer.index(PROCESSING_START)
212
+ candidates << [processing, :processing] if processing
213
+ candidates.min_by(&:first)
214
+ end
215
+
216
+ def start_tag_end(index)
217
+ quote = nil
218
+ i = index
219
+ while i < @buffer.bytesize
220
+ char = @buffer.getbyte(i)
221
+ if quote
222
+ quote = nil if char == quote
223
+ elsif char == 34 || char == 39
224
+ quote = char
225
+ elsif char == 62
226
+ return i
227
+ end
228
+ i += 1
229
+ end
230
+ nil
231
+ end
232
+
233
+ def enclosing_tag_start(index)
234
+ tag_start = @buffer.rindex("<", index - 1)
235
+ return nil unless tag_start
236
+
237
+ tag_end = @buffer.rindex(">", index - 1)
238
+ return nil if tag_end && tag_end > tag_start
239
+
240
+ tag_start
241
+ end
242
+
243
+ def emit_safe_prefix(final:)
244
+ if final
245
+ unless @buffer.empty?
246
+ emit(@buffer.slice!(0, @buffer.bytesize)) { |chunk| yield chunk }
247
+ end
248
+ else
249
+ keep = SCAN_TAIL_BYTES
250
+ length = [@buffer.bytesize - keep, 0].max
251
+ if length.positive?
252
+ emit(@buffer.slice!(0, length)) { |chunk| yield chunk }
253
+ end
254
+ end
255
+ end
256
+
257
+ def discard_safe_prefix(final:)
258
+ keep = final ? 0 : SCAN_TAIL_BYTES
259
+ length = [@buffer.bytesize - keep, 0].max
260
+ discard(@buffer.slice!(0, length)) if length.positive?
261
+ end
262
+
263
+ def emit(chunk)
264
+ yield chunk if chunk && !chunk.empty?
265
+ end
266
+
267
+ def discard(chunk)
268
+ return if chunk.nil? || chunk.empty?
269
+
270
+ @skipped_byte_handler.call(chunk.bytesize)
271
+ end
272
+
273
+ def parse_attributes(tag)
274
+ tag
275
+ .scan(ATTR)
276
+ .each_with_object({}) do |(name, _quote, value), attrs|
277
+ attrs[name] = value
278
+ end
279
+ end
280
+
281
+ def decoded_attribute(value)
282
+ return "" if value.nil?
283
+
284
+ value =
285
+ value
286
+ .dup
287
+ .force_encoding("UTF-8")
288
+ .encode("UTF-8", invalid: :replace, undef: :replace)
289
+ unescape_xml_attribute(value)
290
+ end
291
+
292
+ def unescape_xml_attribute(value)
293
+ value.gsub(/&(?:amp|lt|gt|quot|apos|#\d+|#x[0-9a-fA-F]+);/) do |entity|
294
+ case entity
295
+ when "&amp;"
296
+ "&"
297
+ when "&lt;"
298
+ "<"
299
+ when "&gt;"
300
+ ">"
301
+ when "&quot;"
302
+ '"'
303
+ when "&apos;"
304
+ "'"
305
+ when /\A&#(\d+);\z/
306
+ Regexp.last_match(1).to_i.chr(Encoding::UTF_8)
307
+ when /\A&#x([0-9a-fA-F]+);\z/
308
+ Regexp.last_match(1).to_i(16).chr(Encoding::UTF_8)
309
+ else
310
+ entity
311
+ end
312
+ rescue RangeError
313
+ entity
314
+ end
315
+ end
316
+
317
+ def skip_markers(type)
318
+ case type
319
+ when :cdata
320
+ [CDATA_START, CDATA_END]
321
+ when :comment
322
+ [COMMENT_START, COMMENT_END]
323
+ when :processing
324
+ [PROCESSING_START, PROCESSING_END]
325
+ end
326
+ end
327
+
328
+ def self_closing_tag?(tag)
329
+ tag.to_s.b.sub(/>\z/n, "").rstrip.end_with?("/".b)
330
+ end
331
+
332
+ def trim_oversized_partial_tag
333
+ return if @buffer.bytesize <= XML_CHUNK_SIZE + SCAN_TAIL_BYTES
334
+
335
+ if @skipping_table
336
+ discard(@buffer.slice!(0, @buffer.bytesize - SCAN_TAIL_BYTES))
337
+ else
338
+ @buffer = @buffer.byteslice(1, @buffer.bytesize - 1) || +"".b
339
+ end
340
+ end
341
+ end
342
+ end
343
+ end