json_csv 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/json_csv/csv_builder.rb +14 -5
- data/lib/json_csv/csv_to_json.rb +39 -16
- data/lib/json_csv/json_to_csv.rb +13 -10
- data/lib/json_csv/utils.rb +42 -17
- data/lib/json_csv/version.rb +3 -3
- data/lib/json_csv.rb +5 -3
- data/lib/tasks/json_csv/ci.rake +8 -7
- data/lib/tasks/json_csv.rake +4 -4
- metadata +9 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fca9d1007e87cc6543be68112f80a858d7cadc7bdc409b9643b300167c26a9a7
|
4
|
+
data.tar.gz: 6205ee5d616c4c5dd3cd456a5806b9705bf3356723310c1284a2a41eb3131381
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ec26fe5b50b337de6e7de99a2698c311fbe606343605bfa7a296c5b4f2f39be2b9a21969af107758b99d3e2332b346c06dc3f7b007b7f1c8e435d03609c128fc
|
7
|
+
data.tar.gz: fb0f10c4b581af63325bd69cec48158fca497e0a29c5a416c4df415c53249d7b01f1668b3b55660382c87dc378ff2697c942a01a8b15eda8ebae039d309c23b0
|
data/lib/json_csv/csv_builder.rb
CHANGED
@@ -1,10 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'csv'
|
2
4
|
require 'json_csv/json_to_csv'
|
3
5
|
|
4
6
|
module JsonCsv
|
5
7
|
class CsvBuilder
|
6
|
-
|
7
|
-
|
8
|
+
# private constructor. we don't want users to initialize this class.
|
9
|
+
private_class_method :new
|
10
|
+
|
11
|
+
# map of all headers seen by this CsvBuilder, mapped to their column order indexes
|
12
|
+
attr_reader :known_headers_to_indexes
|
8
13
|
|
9
14
|
def initialize(open_csv_handle)
|
10
15
|
@known_headers_to_indexes = {}
|
@@ -15,7 +20,10 @@ module JsonCsv
|
|
15
20
|
def add(json_hash)
|
16
21
|
row_to_write = []
|
17
22
|
JsonCsv.json_hash_to_flat_csv_row_hash(json_hash).each do |column_header, cell_value|
|
18
|
-
|
23
|
+
unless known_headers_to_indexes.key?(column_header)
|
24
|
+
known_headers_to_indexes[column_header] =
|
25
|
+
known_headers_to_indexes.length
|
26
|
+
end
|
19
27
|
row_to_write[known_headers_to_indexes[column_header]] = cell_value
|
20
28
|
end
|
21
29
|
@open_csv_handle << row_to_write
|
@@ -41,13 +49,14 @@ module JsonCsv
|
|
41
49
|
|
42
50
|
def self.original_header_indexes_to_sorted_indexes(csv_headers, column_header_comparator)
|
43
51
|
original_headers_to_indexes = Hash[csv_headers.map.with_index { |header, index| [header, index] }]
|
44
|
-
headers_to_sorted_indexes = Hash[csv_headers.sort(&column_header_comparator).map.with_index
|
52
|
+
headers_to_sorted_indexes = Hash[csv_headers.sort(&column_header_comparator).map.with_index do |header, index|
|
53
|
+
[header, index]
|
54
|
+
end ]
|
45
55
|
original_to_sorted_index_map = {}
|
46
56
|
original_headers_to_indexes.each do |header, original_index|
|
47
57
|
original_to_sorted_index_map[original_index] = headers_to_sorted_indexes[header]
|
48
58
|
end
|
49
59
|
original_to_sorted_index_map
|
50
60
|
end
|
51
|
-
|
52
61
|
end
|
53
62
|
end
|
data/lib/json_csv/csv_to_json.rb
CHANGED
@@ -1,13 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'json_csv/utils'
|
2
4
|
require 'csv'
|
3
5
|
|
4
6
|
module JsonCsv
|
5
7
|
module CsvToJson
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
TYPE_BOOLEAN = 'boolean'.freeze
|
8
|
+
TYPE_STRING = 'string'
|
9
|
+
TYPE_INTEGER = 'integer'
|
10
|
+
TYPE_FLOAT = 'float'
|
11
|
+
TYPE_BOOLEAN = 'boolean'
|
11
12
|
FIELD_CASTING_TYPES = [TYPE_STRING, TYPE_INTEGER, TYPE_FLOAT, TYPE_BOOLEAN].freeze
|
12
13
|
|
13
14
|
def self.included(base)
|
@@ -19,9 +20,14 @@ module JsonCsv
|
|
19
20
|
# presenting that row as un-flattened json.
|
20
21
|
# This method works for large CSVs and uses very little memory
|
21
22
|
# because it only keeps one row in memory at a time.
|
22
|
-
# Sample usage:
|
23
|
+
# Sample usage:
|
24
|
+
# csv_file_to_hierarchical_json_hash(
|
25
|
+
# path_to_csv, field_casting_rules = {}, strip_value_whitespace = true
|
26
|
+
# ) { |row_json_hash, row_number| ...your block logic here... }
|
23
27
|
def csv_file_to_hierarchical_json_hash(path_to_csv, field_casting_rules = {}, strip_value_whitespace = true)
|
24
|
-
|
28
|
+
# Start with row 2 because this corresponds to the SECOND row of
|
29
|
+
# 1-indexed CSV data (where headers are row 1)
|
30
|
+
i = 2
|
25
31
|
CSV.foreach(path_to_csv, headers: true, header_converters: lambda { |header|
|
26
32
|
header.strip # remove leading and trailing header whitespace
|
27
33
|
}) do |row_data_hash|
|
@@ -34,6 +40,7 @@ module JsonCsv
|
|
34
40
|
hierarchical_hash = {}
|
35
41
|
row_data_hash.each do |key, value|
|
36
42
|
next if value.nil? || value == '' # ignore nil or empty string values
|
43
|
+
|
37
44
|
put_value_at_json_path(hierarchical_hash, key, value, field_casting_rules)
|
38
45
|
end
|
39
46
|
# Clean up empty array elements, which may have come about from CSV data
|
@@ -53,11 +60,19 @@ module JsonCsv
|
|
53
60
|
if json_path_pieces.length == 1
|
54
61
|
# If the full_json_path_from_top matches one of the field_casting_rules,
|
55
62
|
# then case this field to the specified cast type
|
56
|
-
full_json_path_from_top_as_field_casting_rule_pattern =
|
57
|
-
|
63
|
+
full_json_path_from_top_as_field_casting_rule_pattern =
|
64
|
+
real_json_path_to_field_casting_rule_pattern(full_json_path_from_top)
|
65
|
+
obj[json_path_pieces[0]] =
|
66
|
+
if field_casting_rules.key?(full_json_path_from_top_as_field_casting_rule_pattern)
|
67
|
+
apply_field_casting_type(value,
|
68
|
+
field_casting_rules[full_json_path_from_top_as_field_casting_rule_pattern])
|
69
|
+
else
|
70
|
+
value
|
71
|
+
end
|
58
72
|
else
|
59
73
|
obj[json_path_pieces[0]] ||= (json_path_pieces[1].is_a?(Integer) ? [] : {})
|
60
|
-
put_value_at_json_path(obj[json_path_pieces[0]], pieces_to_json_path(json_path_pieces[1
|
74
|
+
put_value_at_json_path(obj[json_path_pieces[0]], pieces_to_json_path(json_path_pieces[1..]), value,
|
75
|
+
field_casting_rules, full_json_path_from_top)
|
61
76
|
end
|
62
77
|
end
|
63
78
|
|
@@ -68,19 +83,28 @@ module JsonCsv
|
|
68
83
|
end
|
69
84
|
|
70
85
|
def apply_field_casting_type(value, field_casting_type)
|
71
|
-
|
86
|
+
unless FIELD_CASTING_TYPES.include?(field_casting_type)
|
87
|
+
raise ArgumentError,
|
88
|
+
"Invalid cast type #{field_casting_type}"
|
89
|
+
end
|
72
90
|
|
73
91
|
case field_casting_type
|
74
92
|
when TYPE_INTEGER
|
75
|
-
raise ArgumentError, "\"#{value}\" is not an integer" unless
|
93
|
+
raise ArgumentError, "\"#{value}\" is not an integer" unless /^[0-9]+$/.match?(value.to_s)
|
94
|
+
|
76
95
|
value.to_i
|
77
96
|
when TYPE_FLOAT
|
78
|
-
|
97
|
+
unless value.to_s =~ /^[0-9]+(\.[0-9]+)*$/ || value.to_s =~ /^\.[0-9]+$/
|
98
|
+
raise ArgumentError,
|
99
|
+
"\"#{value}\" is not a float"
|
100
|
+
end
|
101
|
+
|
79
102
|
value.to_f
|
80
103
|
when TYPE_BOOLEAN
|
81
|
-
|
104
|
+
case value.downcase
|
105
|
+
when 'true'
|
82
106
|
true
|
83
|
-
|
107
|
+
when 'false'
|
84
108
|
false
|
85
109
|
else
|
86
110
|
raise ArgumentError, "\"#{value}\" is not a boolean"
|
@@ -123,7 +147,6 @@ module JsonCsv
|
|
123
147
|
end
|
124
148
|
json_path
|
125
149
|
end
|
126
|
-
|
127
150
|
end
|
128
151
|
end
|
129
152
|
end
|
data/lib/json_csv/json_to_csv.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'json'
|
2
4
|
require 'json_csv/csv_builder'
|
3
5
|
|
4
6
|
module JsonCsv
|
5
7
|
module JsonToCsv
|
6
|
-
|
7
8
|
def self.included(base)
|
8
9
|
base.extend ClassMethods
|
9
10
|
end
|
@@ -26,21 +27,20 @@ module JsonCsv
|
|
26
27
|
# csv_builder.add(json_hash)
|
27
28
|
# end
|
28
29
|
# end
|
29
|
-
def create_csv_for_json_records(csv_outfile_path, header_sort_comparator = DEFAULT_HEADER_SORT_COMPARATOR)
|
30
|
-
csv_temp_outfile_path = csv_outfile_path
|
30
|
+
def create_csv_for_json_records(csv_outfile_path, header_sort_comparator = DEFAULT_HEADER_SORT_COMPARATOR, &block)
|
31
|
+
csv_temp_outfile_path = "#{csv_outfile_path}.temp"
|
31
32
|
|
32
33
|
begin
|
33
34
|
# Step 1: Build CSV with unsorted headers in temp file
|
34
|
-
csv_headers = JsonCsv::CsvBuilder.create_csv_without_headers(csv_temp_outfile_path, 'wb')
|
35
|
-
yield csv_builder
|
36
|
-
end
|
35
|
+
csv_headers = JsonCsv::CsvBuilder.create_csv_without_headers(csv_temp_outfile_path, 'wb', &block)
|
37
36
|
|
38
37
|
# Step 2: Sort CSV columns by header, based on column_header_comparator
|
39
|
-
original_to_sorted_index_map = JsonCsv::CsvBuilder.original_header_indexes_to_sorted_indexes(
|
38
|
+
original_to_sorted_index_map = JsonCsv::CsvBuilder.original_header_indexes_to_sorted_indexes(
|
39
|
+
csv_headers, header_sort_comparator
|
40
|
+
)
|
40
41
|
CSV.open(csv_outfile_path, 'wb') do |final_csv|
|
41
42
|
# Open temporary CSV for reading
|
42
43
|
CSV.open(csv_temp_outfile_path, 'rb') do |temp_csv|
|
43
|
-
|
44
44
|
# write out ordered header row
|
45
45
|
reordered_header_row = []
|
46
46
|
csv_headers.each_with_index do |header, index|
|
@@ -82,8 +82,11 @@ module JsonCsv
|
|
82
82
|
if obj.is_a?(Hash)
|
83
83
|
obj.each do |key, val|
|
84
84
|
if key_contains_unallowed_characters?(key)
|
85
|
-
raise ArgumentError,
|
85
|
+
raise ArgumentError,
|
86
|
+
'Cannot deal with hash keys that contain "[" or "]" or "." because '\
|
87
|
+
'these characters have special meanings in CSV headers.'
|
86
88
|
end
|
89
|
+
|
87
90
|
path = parent_path + (parent_path.empty? ? '' : '.') + key
|
88
91
|
flatten_hash(val, path, flat_hash_to_build)
|
89
92
|
end
|
@@ -101,9 +104,9 @@ module JsonCsv
|
|
101
104
|
|
102
105
|
def key_contains_unallowed_characters?(key)
|
103
106
|
return true if key.index('[') || key.index(']') || key.index('.')
|
107
|
+
|
104
108
|
false
|
105
109
|
end
|
106
110
|
end
|
107
|
-
|
108
111
|
end
|
109
112
|
end
|
data/lib/json_csv/utils.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module JsonCsv
|
2
4
|
module Utils
|
3
|
-
|
4
5
|
# Returns true for empty strings, empty arrays, or empty hashes.
|
5
6
|
# Also returns true for strings that only contain whitespace.
|
6
7
|
# Returns false for all other values, including booleans and numbers.
|
@@ -8,33 +9,41 @@ module JsonCsv
|
|
8
9
|
return true if value.respond_to?(:empty?) && value.empty? # empty string, empty array, or empty hash
|
9
10
|
return true if value.is_a?(String) && value.strip.empty? # string that only contains whitespace
|
10
11
|
return true if value.nil?
|
12
|
+
|
11
13
|
false
|
12
14
|
end
|
13
15
|
|
16
|
+
def self.hash_or_array?(obj)
|
17
|
+
obj.is_a?(Hash) || obj.is_a?(Array)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Given a Hash or Array, recursively removes all blank fields.
|
21
|
+
# Note: This method will raise an ArgumentError if the supplied object
|
22
|
+
# is a frozen Hash or Array.
|
14
23
|
def self.recursively_remove_blank_fields!(hash_or_array)
|
15
|
-
|
24
|
+
raise ArgumentError, 'Must supply a Hash or Array' unless hash_or_array?(hash_or_array)
|
25
|
+
raise ArgumentError, "Cannot modify frozen value: #{hash_or_array.inspect}" if hash_or_array.frozen?
|
16
26
|
|
17
|
-
|
27
|
+
case hash_or_array
|
28
|
+
when Array
|
18
29
|
# Recurse through non-empty elements
|
19
30
|
hash_or_array.each do |element|
|
20
|
-
recursively_remove_blank_fields!(element) if
|
31
|
+
recursively_remove_blank_fields!(element) if hash_or_array?(element)
|
21
32
|
end
|
22
33
|
|
23
34
|
# Delete blank array element values on this array level (including empty object ({}) values)
|
24
35
|
hash_or_array.delete_if do |element|
|
25
36
|
removable_value?(element)
|
26
37
|
end
|
27
|
-
|
38
|
+
when Hash
|
28
39
|
hash_or_array.each_value do |value|
|
29
|
-
recursively_remove_blank_fields!(value) if
|
40
|
+
recursively_remove_blank_fields!(value) if hash_or_array?(value)
|
30
41
|
end
|
31
42
|
|
32
43
|
# Delete blank hash values on this hash level (including empty object ({}) values)
|
33
44
|
hash_or_array.delete_if do |_key, value|
|
34
45
|
removable_value?(value)
|
35
46
|
end
|
36
|
-
else
|
37
|
-
raise ArgumentError, 'Must supply a hash or array.'
|
38
47
|
end
|
39
48
|
|
40
49
|
hash_or_array
|
@@ -44,21 +53,37 @@ module JsonCsv
|
|
44
53
|
# modifying the object's nested child hashes or array.
|
45
54
|
# Note: This method modifies hash values, but does not
|
46
55
|
# modify hash keys.
|
47
|
-
def self.recursively_strip_value_whitespace!(obj)
|
48
|
-
|
49
|
-
|
50
|
-
|
56
|
+
def self.recursively_strip_value_whitespace!(obj, replace_frozen_strings_when_stripped: false)
|
57
|
+
case obj
|
58
|
+
when Array
|
59
|
+
obj.each_with_index do |element, ix|
|
60
|
+
if element.is_a?(String) && replace_frozen_strings_when_stripped
|
61
|
+
stripped_string = element.strip
|
62
|
+
obj[ix] = stripped_string if stripped_string != obj[ix]
|
63
|
+
else
|
64
|
+
recursively_strip_value_whitespace!(
|
65
|
+
element,
|
66
|
+
replace_frozen_strings_when_stripped: replace_frozen_strings_when_stripped
|
67
|
+
)
|
68
|
+
end
|
51
69
|
end
|
52
|
-
|
53
|
-
obj.
|
54
|
-
|
70
|
+
when Hash
|
71
|
+
obj.each do |key, value|
|
72
|
+
if value.is_a?(String) && replace_frozen_strings_when_stripped
|
73
|
+
stripped_string = obj[key].strip
|
74
|
+
obj[key] = stripped_string if stripped_string != obj[key]
|
75
|
+
else
|
76
|
+
recursively_strip_value_whitespace!(
|
77
|
+
value,
|
78
|
+
replace_frozen_strings_when_stripped: replace_frozen_strings_when_stripped
|
79
|
+
)
|
80
|
+
end
|
55
81
|
end
|
56
|
-
|
82
|
+
when String
|
57
83
|
obj.strip!
|
58
84
|
end
|
59
85
|
|
60
86
|
obj
|
61
87
|
end
|
62
|
-
|
63
88
|
end
|
64
89
|
end
|
data/lib/json_csv/version.rb
CHANGED
data/lib/json_csv.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json_csv/version'
|
4
|
+
require 'json_csv/json_to_csv'
|
5
|
+
require 'json_csv/csv_to_json'
|
4
6
|
|
5
7
|
module JsonCsv
|
6
8
|
include JsonCsv::JsonToCsv
|
data/lib/tasks/json_csv/ci.rake
CHANGED
@@ -1,7 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'json_csv'
|
2
4
|
|
3
5
|
namespace :json_csv do
|
4
|
-
|
5
6
|
begin
|
6
7
|
# This code is in a begin/rescue block so that the Rakefile is usable
|
7
8
|
# in an environment where RSpec is unavailable (i.e. production).
|
@@ -21,18 +22,18 @@ namespace :json_csv do
|
|
21
22
|
task.fail_on_error = true
|
22
23
|
end
|
23
24
|
rescue LoadError => e
|
24
|
-
puts
|
25
|
+
puts '[Warning] Exception creating rspec rake tasks. This message can be ignored in '\
|
26
|
+
'environments that intentionally do not pull in the RSpec gem (i.e. production).'
|
25
27
|
puts e
|
26
28
|
end
|
27
29
|
|
28
|
-
desc
|
30
|
+
desc 'CI build'
|
29
31
|
task ci: ['json_csv:rubocop'] do
|
30
|
-
Rake::Task[
|
32
|
+
Rake::Task['json_csv:rspec'].invoke
|
31
33
|
end
|
32
34
|
|
33
|
-
desc
|
35
|
+
desc 'CI build'
|
34
36
|
task :ci_nocop do
|
35
|
-
Rake::Task[
|
37
|
+
Rake::Task['json_csv:rspec'].invoke
|
36
38
|
end
|
37
|
-
|
38
39
|
end
|
data/lib/tasks/json_csv.rake
CHANGED
metadata
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: json_csv
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eric O'Hanlon
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
date: 2018-02-18 00:00:00.000000000 Z
|
@@ -39,33 +39,19 @@ dependencies:
|
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '3.7'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
42
|
+
name: rubocul
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: 0.
|
47
|
+
version: 4.0.6
|
48
48
|
type: :development
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: 0.
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: rubocop-rspec
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - "~>"
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: 1.20.1
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - "~>"
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: 1.20.1
|
54
|
+
version: 4.0.6
|
69
55
|
- !ruby/object:Gem::Dependency
|
70
56
|
name: simplecov
|
71
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -99,7 +85,7 @@ homepage: https://github.com/cul/json_csv
|
|
99
85
|
licenses:
|
100
86
|
- MIT
|
101
87
|
metadata: {}
|
102
|
-
post_install_message:
|
88
|
+
post_install_message:
|
103
89
|
rdoc_options: []
|
104
90
|
require_paths:
|
105
91
|
- lib
|
@@ -107,15 +93,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
107
93
|
requirements:
|
108
94
|
- - ">="
|
109
95
|
- !ruby/object:Gem::Version
|
110
|
-
version:
|
96
|
+
version: 2.6.0
|
111
97
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
98
|
requirements:
|
113
99
|
- - ">="
|
114
100
|
- !ruby/object:Gem::Version
|
115
101
|
version: '0'
|
116
102
|
requirements: []
|
117
|
-
rubygems_version: 3.0.
|
118
|
-
signing_key:
|
103
|
+
rubygems_version: 3.0.3.1
|
104
|
+
signing_key:
|
119
105
|
specification_version: 4
|
120
106
|
summary: A library for converting json to csv...and back!
|
121
107
|
test_files: []
|