export_kit 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +13 -0
- data/LICENSE.txt +21 -0
- data/README.md +242 -0
- data/README.pl.md +113 -0
- data/lib/export_kit/active_record.rb +16 -0
- data/lib/export_kit/artifact.rb +22 -0
- data/lib/export_kit/document.rb +165 -0
- data/lib/export_kit/format_registry.rb +15 -0
- data/lib/export_kit/model_rows.rb +96 -0
- data/lib/export_kit/model_schema.rb +96 -0
- data/lib/export_kit/model_serializer.rb +265 -0
- data/lib/export_kit/output.rb +58 -0
- data/lib/export_kit/render.rb +187 -0
- data/lib/export_kit/renderers/base.rb +94 -0
- data/lib/export_kit/renderers/csv_archive.rb +108 -0
- data/lib/export_kit/renderers/json.rb +69 -0
- data/lib/export_kit/renderers/pdf.rb +127 -0
- data/lib/export_kit/result.rb +68 -0
- data/lib/export_kit/table.rb +41 -0
- data/lib/export_kit/validate_artifact.rb +296 -0
- data/lib/export_kit/version.rb +5 -0
- data/lib/export_kit.rb +27 -0
- metadata +134 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "csv"
|
|
4
|
+
require "digest"
|
|
5
|
+
require "zip"
|
|
6
|
+
|
|
7
|
+
module ExportKit
|
|
8
|
+
# Generic verification of a rendered artifact against the Document that produced it. A concrete
|
|
9
|
+
# workflow may check a few domain identifiers before calling this class, but never reimplements
|
|
10
|
+
# digest, manifest or JSON/CSV/PDF validation.
|
|
11
|
+
class ValidateArtifact
|
|
12
|
+
INVALID = :export_artifact_invalid
|
|
13
|
+
PLAIN_VALUES = [String, Integer, Float, TrueClass, FalseClass, NilClass].freeze
|
|
14
|
+
|
|
15
|
+
def self.call(**args) = new(**args).call
|
|
16
|
+
|
|
17
|
+
def initialize(built:, failure_code: INVALID)
|
|
18
|
+
@built = built
|
|
19
|
+
@failure_code = failure_code
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call
|
|
23
|
+
valid = digest_matches? && valid_contents?
|
|
24
|
+
valid ? success(built) : failure(failure_code)
|
|
25
|
+
rescue JSON::ParserError, CSV::MalformedCSVError, Zip::Error, EncodingError, Errno::ENOENT, IOError
|
|
26
|
+
failure(failure_code)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
attr_reader :built, :failure_code
|
|
32
|
+
|
|
33
|
+
def document = built.document
|
|
34
|
+
|
|
35
|
+
def success(value) = Result.success(value)
|
|
36
|
+
def failure(code) = Result.failure(code)
|
|
37
|
+
|
|
38
|
+
def digest_matches?
|
|
39
|
+
built.file.path && File.size(built.file.path) == built.byte_size &&
|
|
40
|
+
Digest::SHA256.file(built.file.path).hexdigest == built.sha256
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def valid_contents?
|
|
44
|
+
return valid_simple_contents? unless document.is_a?(Document)
|
|
45
|
+
|
|
46
|
+
valid_artifact_metadata? && valid_manifest? && valid_format?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def valid_simple_contents?
|
|
50
|
+
case built.format
|
|
51
|
+
when "json" then valid_simple_json?
|
|
52
|
+
when "csv" then valid_simple_csv?
|
|
53
|
+
when "pdf" then valid_pdf?
|
|
54
|
+
else false
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def valid_simple_json?
|
|
59
|
+
value = JSON.parse(File.read(built.file.path))
|
|
60
|
+
!document.is_a?(Table) || (value.is_a?(Array) && value.all? { valid_record?(it, document) })
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def valid_simple_csv?
|
|
64
|
+
return false unless document.is_a?(Table)
|
|
65
|
+
|
|
66
|
+
table = CSV.parse(File.read(built.file.path), headers: true)
|
|
67
|
+
table.headers == expected_csv_headers(document) && table.all? { it.size == document.columns.size }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def valid_artifact_metadata?
|
|
71
|
+
definition = ExportKit::FormatRegistry.find(built.format == "csv" ? "csv_zip" : built.format)
|
|
72
|
+
return false unless definition
|
|
73
|
+
|
|
74
|
+
actual = [built.identifier, built.schema_version, built.content_type, built.extension, built.metadata]
|
|
75
|
+
expected = [document.identifier, document.schema_version, definition.content_type, definition.extension,
|
|
76
|
+
document.metadata]
|
|
77
|
+
actual == expected
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def valid_manifest?
|
|
81
|
+
return false unless built.manifest.is_a?(Array) && built.manifest.size == document.sections.size
|
|
82
|
+
|
|
83
|
+
document.sections.zip(built.manifest).all? { |section, entry| valid_manifest_entry?(section, entry) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def valid_manifest_entry?(section, entry)
|
|
87
|
+
return false unless entry.is_a?(Hash)
|
|
88
|
+
|
|
89
|
+
counts = entry["record_sets"]
|
|
90
|
+
valid_manifest_counts?(section, counts) && entry == expected_manifest_entry(section, counts)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def expected_manifest_entry(section, counts)
|
|
94
|
+
{
|
|
95
|
+
"category" => section.key, "label" => section.label, "description" => section.description,
|
|
96
|
+
**section.metadata, "record_sets" => counts, "count" => counts&.values&.sum
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def valid_manifest_counts?(section, counts)
|
|
101
|
+
counts.is_a?(Hash) && counts.keys == section.record_sets.map(&:name) &&
|
|
102
|
+
counts.values.all? { it.is_a?(Integer) && it >= 0 }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def valid_format?
|
|
106
|
+
case built.format
|
|
107
|
+
when "json" then valid_json?
|
|
108
|
+
when "csv_zip", "csv" then valid_csv_archive?
|
|
109
|
+
when "pdf" then valid_pdf?
|
|
110
|
+
else false
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def valid_json?
|
|
115
|
+
document = JSON.parse(File.read(built.file.path))
|
|
116
|
+
valid_json_shape?(document) && valid_json_envelope?(document) &&
|
|
117
|
+
document["manifest"] == built.manifest && valid_json_data?(document["data"])
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def valid_json_shape?(document)
|
|
121
|
+
expected_keys = (built.metadata.keys + %w(format schema_version manifest data)).sort
|
|
122
|
+
document.is_a?(Hash) && document.keys.sort == expected_keys
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def valid_json_envelope?(document)
|
|
126
|
+
document.except("format", "schema_version", "manifest", "data") == built.metadata &&
|
|
127
|
+
document["format"] == built.identifier && document["schema_version"] == built.schema_version
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def valid_json_data?(data)
|
|
131
|
+
categories = document.sections.map(&:key)
|
|
132
|
+
return false unless data.is_a?(Hash) && data.keys == categories
|
|
133
|
+
|
|
134
|
+
built.manifest.all? { |entry| valid_json_category?(data.fetch(entry["category"]), entry) }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def valid_json_category?(sets, entry)
|
|
138
|
+
record_sets = section(entry.fetch("category")).record_sets
|
|
139
|
+
return false unless sets.is_a?(Hash) && sets.keys == record_sets.map(&:name) && counted?(sets, entry)
|
|
140
|
+
|
|
141
|
+
record_sets.all? { |record_set| sets.fetch(record_set.name).all? { valid_record?(it, record_set) } }
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def counted?(sets, entry)
|
|
145
|
+
entry["record_sets"] == sets.transform_values { it.is_a?(Array) ? it.size : -1 } &&
|
|
146
|
+
entry["count"] == entry["record_sets"].values.sum
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def valid_record?(record, record_set)
|
|
150
|
+
record.is_a?(Hash) && record.keys == record_set.columns && record.values.all? { plain?(it) }
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def plain?(value)
|
|
154
|
+
return value.all? { |item| PLAIN_VALUES.any? { item.is_a?(it) } } if value.is_a?(Array)
|
|
155
|
+
|
|
156
|
+
PLAIN_VALUES.any? { value.is_a?(it) }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def valid_csv_archive?
|
|
160
|
+
Zip::File.open(built.file.path) do |archive|
|
|
161
|
+
next false unless valid_csv_entries?(archive)
|
|
162
|
+
next false unless valid_csv_manifest?(csv_entry_contents(archive, "manifest.csv"))
|
|
163
|
+
|
|
164
|
+
valid_csv_record_sets?(archive)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def valid_csv_entries?(archive)
|
|
169
|
+
names = archive.entries.map(&:name)
|
|
170
|
+
names.sort == ["manifest.csv", *expected_csv_files].sort && names.all? { safe_zip_path?(it) }
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def expected_csv_files
|
|
174
|
+
built.manifest.flat_map do |entry|
|
|
175
|
+
entry.fetch("record_sets").filter_map do |record_set, count|
|
|
176
|
+
"data/#{entry.fetch('category')}/#{record_set}.csv" if count.positive?
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def safe_zip_path?(name)
|
|
182
|
+
name == "manifest.csv" || name.match?(%r{\Adata/[a-z][a-z0-9_]*/[a-z][a-z0-9_]*\.csv\z})
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def valid_csv_manifest?(contents)
|
|
186
|
+
rows = CSV.parse(contents, headers: true)
|
|
187
|
+
return false unless rows.headers == ExportKit::Renderers::CsvArchive::MANIFEST_COLUMNS
|
|
188
|
+
return false unless rows.size == expected_csv_manifest_rows
|
|
189
|
+
|
|
190
|
+
valid_csv_document_manifest?(rows) && valid_csv_section_manifest?(rows) && valid_csv_record_set_manifest?(rows)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def expected_csv_manifest_rows
|
|
194
|
+
3 + built.metadata.size + document.sections.sum { 1 + it.record_sets.size }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def valid_csv_document_manifest?(rows)
|
|
198
|
+
expected = expected_csv_document_metadata
|
|
199
|
+
entries = csv_document_metadata_entries(rows)
|
|
200
|
+
|
|
201
|
+
valid_csv_document_entries?(entries, expected) && entries.reduce({}, :merge) == expected
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def expected_csv_document_metadata
|
|
205
|
+
{ "format" => built.identifier, "schema_version" => built.schema_version,
|
|
206
|
+
"title" => document.title, **built.metadata }
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def csv_document_metadata_entries(rows)
|
|
210
|
+
rows.filter_map { JSON.parse(it["metadata"]) if it["kind"] == "document" }
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def valid_csv_document_entries?(entries, expected)
|
|
214
|
+
entries.all? { it.is_a?(Hash) && it.one? } && entries.flat_map(&:keys).sort == expected.keys.sort
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def valid_csv_section_manifest?(rows)
|
|
218
|
+
built.manifest.all? do |entry|
|
|
219
|
+
row = sole_manifest_row(rows, "section", entry.fetch("category"))
|
|
220
|
+
row && row["count"].to_i == entry.fetch("count") &&
|
|
221
|
+
JSON.parse(row["metadata"]) == entry.except("category", "label", "description", "record_sets", "count")
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def valid_csv_record_set_manifest?(rows)
|
|
226
|
+
built.manifest.all? do |entry|
|
|
227
|
+
section(entry.fetch("category")).record_sets.all? { valid_csv_record_set_manifest_row?(rows, entry, it) }
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def valid_csv_record_set_manifest_row?(rows, entry, record_set)
|
|
232
|
+
row = sole_manifest_row(rows, "record_set", entry.fetch("category"), record_set.name)
|
|
233
|
+
count = entry.fetch("record_sets").fetch(record_set.name)
|
|
234
|
+
row && row["file"] == expected_csv_file(entry, record_set, count) && row["count"].to_i == count &&
|
|
235
|
+
JSON.parse(row["metadata"]) == expected_csv_record_set_metadata(record_set)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def expected_csv_file(entry, record_set, count)
|
|
239
|
+
"data/#{entry.fetch('category')}/#{record_set.name}.csv" if count.positive?
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def expected_csv_record_set_metadata(record_set)
|
|
243
|
+
{ "columns" => record_set.columns, "headers" => expected_csv_headers(record_set) }
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def sole_manifest_row(rows, kind, section, record_set = nil)
|
|
247
|
+
matches = rows.select do |row|
|
|
248
|
+
row.values_at("kind", "section", "record_set") == [kind, section, record_set]
|
|
249
|
+
end
|
|
250
|
+
matches.first if matches.one?
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def valid_csv_record_sets?(archive)
|
|
254
|
+
built.manifest.all? do |entry|
|
|
255
|
+
section(entry.fetch("category")).record_sets.all? { valid_csv_record_set?(archive, entry, it) }
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def valid_csv_record_set?(archive, entry, record_set)
|
|
260
|
+
count = entry.fetch("record_sets").fetch(record_set.name)
|
|
261
|
+
return count.zero? unless count.positive?
|
|
262
|
+
|
|
263
|
+
path = "data/#{entry.fetch('category')}/#{record_set.name}.csv"
|
|
264
|
+
table = CSV.parse(csv_entry_contents(archive, path), headers: true)
|
|
265
|
+
table.headers == expected_csv_headers(record_set) && table.size == count
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def csv_entry_contents(archive, path)
|
|
269
|
+
utf8(archive.find_entry(path).get_input_stream.read)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def expected_csv_headers(record_set)
|
|
273
|
+
record_set.headers.map { Renderers::CsvArchive.csv_value(it) }
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def section(key)
|
|
277
|
+
document.sections.find { it.key == key }
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def valid_pdf?
|
|
281
|
+
File.open(built.file.path, "rb") do |file|
|
|
282
|
+
file.read(8)&.start_with?("%PDF-") && begin
|
|
283
|
+
file.seek(-[file.size, 1024].min, IO::SEEK_END)
|
|
284
|
+
file.read.include?("%%EOF")
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def utf8(contents)
|
|
290
|
+
encoded = contents.force_encoding(Encoding::UTF_8)
|
|
291
|
+
raise EncodingError, "export CSV is not UTF-8" unless encoded.valid_encoding?
|
|
292
|
+
|
|
293
|
+
encoded
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|
data/lib/export_kit.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "csv"
|
|
5
|
+
require "digest"
|
|
6
|
+
require "tempfile"
|
|
7
|
+
require "export_kit/version"
|
|
8
|
+
require "export_kit/result"
|
|
9
|
+
require "export_kit/document"
|
|
10
|
+
require "export_kit/table"
|
|
11
|
+
require "export_kit/renderers/base"
|
|
12
|
+
require "export_kit/output"
|
|
13
|
+
require "export_kit/renderers/json"
|
|
14
|
+
require "export_kit/renderers/csv_archive"
|
|
15
|
+
require "export_kit/renderers/pdf"
|
|
16
|
+
require "export_kit/format_registry"
|
|
17
|
+
require "export_kit/validate_artifact"
|
|
18
|
+
require "export_kit/artifact"
|
|
19
|
+
require "export_kit/render"
|
|
20
|
+
|
|
21
|
+
module ExportKit
|
|
22
|
+
def self.render(data, **args) = Render.new(data:, **args).call
|
|
23
|
+
|
|
24
|
+
def self.validate(artifact, failure_code: ValidateArtifact::INVALID)
|
|
25
|
+
ValidateArtifact.call(built: artifact, failure_code:)
|
|
26
|
+
end
|
|
27
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: export_kit
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Łukasz Kerl
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: csv
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: 3.3.6
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '4'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: 3.3.6
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '4'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: json
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: 2.21.2
|
|
39
|
+
- - "<"
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '4'
|
|
42
|
+
type: :runtime
|
|
43
|
+
prerelease: false
|
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: 2.21.2
|
|
49
|
+
- - "<"
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '4'
|
|
52
|
+
- !ruby/object:Gem::Dependency
|
|
53
|
+
name: prawn
|
|
54
|
+
requirement: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - "~>"
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '2.5'
|
|
59
|
+
type: :runtime
|
|
60
|
+
prerelease: false
|
|
61
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
62
|
+
requirements:
|
|
63
|
+
- - "~>"
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '2.5'
|
|
66
|
+
- !ruby/object:Gem::Dependency
|
|
67
|
+
name: rubyzip
|
|
68
|
+
requirement: !ruby/object:Gem::Requirement
|
|
69
|
+
requirements:
|
|
70
|
+
- - "~>"
|
|
71
|
+
- !ruby/object:Gem::Version
|
|
72
|
+
version: '3.6'
|
|
73
|
+
type: :runtime
|
|
74
|
+
prerelease: false
|
|
75
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
76
|
+
requirements:
|
|
77
|
+
- - "~>"
|
|
78
|
+
- !ruby/object:Gem::Version
|
|
79
|
+
version: '3.6'
|
|
80
|
+
description: Builds streamed, size-bounded export artifacts with explicit schemas,
|
|
81
|
+
safe CSV cells, structural validation and an optional Active Record adapter.
|
|
82
|
+
executables: []
|
|
83
|
+
extensions: []
|
|
84
|
+
extra_rdoc_files: []
|
|
85
|
+
files:
|
|
86
|
+
- CHANGELOG.md
|
|
87
|
+
- LICENSE.txt
|
|
88
|
+
- README.md
|
|
89
|
+
- README.pl.md
|
|
90
|
+
- lib/export_kit.rb
|
|
91
|
+
- lib/export_kit/active_record.rb
|
|
92
|
+
- lib/export_kit/artifact.rb
|
|
93
|
+
- lib/export_kit/document.rb
|
|
94
|
+
- lib/export_kit/format_registry.rb
|
|
95
|
+
- lib/export_kit/model_rows.rb
|
|
96
|
+
- lib/export_kit/model_schema.rb
|
|
97
|
+
- lib/export_kit/model_serializer.rb
|
|
98
|
+
- lib/export_kit/output.rb
|
|
99
|
+
- lib/export_kit/render.rb
|
|
100
|
+
- lib/export_kit/renderers/base.rb
|
|
101
|
+
- lib/export_kit/renderers/csv_archive.rb
|
|
102
|
+
- lib/export_kit/renderers/json.rb
|
|
103
|
+
- lib/export_kit/renderers/pdf.rb
|
|
104
|
+
- lib/export_kit/result.rb
|
|
105
|
+
- lib/export_kit/table.rb
|
|
106
|
+
- lib/export_kit/validate_artifact.rb
|
|
107
|
+
- lib/export_kit/version.rb
|
|
108
|
+
homepage: https://github.com/carlos-1410/export-kit
|
|
109
|
+
licenses:
|
|
110
|
+
- MIT
|
|
111
|
+
metadata:
|
|
112
|
+
homepage_uri: https://github.com/carlos-1410/export-kit
|
|
113
|
+
source_code_uri: https://github.com/carlos-1410/export-kit/tree/v0.1.0
|
|
114
|
+
changelog_uri: https://github.com/carlos-1410/export-kit/blob/v0.1.0/CHANGELOG.md
|
|
115
|
+
allowed_push_host: https://rubygems.org
|
|
116
|
+
rubygems_mfa_required: 'true'
|
|
117
|
+
rdoc_options: []
|
|
118
|
+
require_paths:
|
|
119
|
+
- lib
|
|
120
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - ">="
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: 3.4.0
|
|
125
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
126
|
+
requirements:
|
|
127
|
+
- - ">="
|
|
128
|
+
- !ruby/object:Gem::Version
|
|
129
|
+
version: '0'
|
|
130
|
+
requirements: []
|
|
131
|
+
rubygems_version: 3.6.9
|
|
132
|
+
specification_version: 4
|
|
133
|
+
summary: Portable, size-bounded JSON, CSV and PDF exports
|
|
134
|
+
test_files: []
|