csvlint 0.4.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.
Files changed (53) hide show
  1. checksums.yaml +5 -5
  2. data/.github/dependabot.yml +11 -0
  3. data/.github/workflows/push.yml +35 -0
  4. data/.gitignore +1 -0
  5. data/.ruby-version +1 -1
  6. data/.standard_todo.yml +43 -0
  7. data/CHANGELOG.md +38 -0
  8. data/Dockerfile +16 -0
  9. data/Gemfile +2 -2
  10. data/README.md +13 -10
  11. data/Rakefile +7 -7
  12. data/bin/create_schema +2 -2
  13. data/csvlint.gemspec +19 -22
  14. data/docker_notes_for_windows.txt +20 -0
  15. data/features/step_definitions/cli_steps.rb +11 -11
  16. data/features/step_definitions/information_steps.rb +4 -4
  17. data/features/step_definitions/parse_csv_steps.rb +11 -11
  18. data/features/step_definitions/schema_validation_steps.rb +10 -10
  19. data/features/step_definitions/sources_steps.rb +1 -1
  20. data/features/step_definitions/validation_errors_steps.rb +19 -19
  21. data/features/step_definitions/validation_info_steps.rb +9 -9
  22. data/features/step_definitions/validation_warnings_steps.rb +11 -11
  23. data/features/support/aruba.rb +10 -9
  24. data/features/support/earl_formatter.rb +39 -39
  25. data/features/support/env.rb +10 -11
  26. data/features/support/load_tests.rb +109 -105
  27. data/features/support/webmock.rb +3 -1
  28. data/lib/csvlint/cli.rb +136 -142
  29. data/lib/csvlint/csvw/column.rb +279 -280
  30. data/lib/csvlint/csvw/date_format.rb +90 -92
  31. data/lib/csvlint/csvw/metadata_error.rb +1 -3
  32. data/lib/csvlint/csvw/number_format.rb +40 -32
  33. data/lib/csvlint/csvw/property_checker.rb +714 -717
  34. data/lib/csvlint/csvw/table.rb +49 -52
  35. data/lib/csvlint/csvw/table_group.rb +24 -23
  36. data/lib/csvlint/error_collector.rb +2 -0
  37. data/lib/csvlint/error_message.rb +0 -1
  38. data/lib/csvlint/field.rb +153 -141
  39. data/lib/csvlint/schema.rb +35 -43
  40. data/lib/csvlint/validate.rb +173 -151
  41. data/lib/csvlint/version.rb +1 -1
  42. data/lib/csvlint.rb +22 -23
  43. data/spec/csvw/column_spec.rb +15 -16
  44. data/spec/csvw/date_format_spec.rb +5 -7
  45. data/spec/csvw/number_format_spec.rb +2 -4
  46. data/spec/csvw/table_group_spec.rb +103 -105
  47. data/spec/csvw/table_spec.rb +71 -73
  48. data/spec/field_spec.rb +116 -121
  49. data/spec/schema_spec.rb +131 -141
  50. data/spec/spec_helper.rb +6 -6
  51. data/spec/validator_spec.rb +167 -203
  52. metadata +41 -85
  53. data/.travis.yml +0 -37
data/lib/csvlint/cli.rb CHANGED
@@ -1,14 +1,13 @@
1
- require 'csvlint'
2
- require 'colorize'
3
- require 'json'
4
- require 'pp'
5
- require 'thor'
1
+ require "csvlint"
2
+ require "rainbow"
3
+ require "active_support/json"
4
+ require "json"
5
+ require "thor"
6
6
 
7
- require 'active_support/inflector'
7
+ require "active_support/inflector"
8
8
 
9
9
  module Csvlint
10
10
  class Cli < Thor
11
-
12
11
  desc "myfile.csv OR csvlint http://example.com/myfile.csv", "Supports validating CSV files to check their syntax and contents"
13
12
 
14
13
  option :dump_errors, desc: "Pretty print error and warning objects.", type: :boolean, aliases: :d
@@ -21,6 +20,8 @@ module Csvlint
21
20
  @schema = get_schema(options[:schema]) if options[:schema]
22
21
  fetch_schema_tables(@schema, options) if source.nil?
23
22
 
23
+ Rainbow.enabled = $stdout.tty?
24
+
24
25
  valid = validate_csv(source, @schema, options[:dump_errors], options[:json], options[:werror])
25
26
  exit 1 unless valid
26
27
  end
@@ -33,174 +34,167 @@ module Csvlint
33
34
 
34
35
  private
35
36
 
36
- def read_source(source)
37
- if source.nil?
38
- # If no source is present, try reading from stdin
39
- if !$stdin.tty?
40
- source = StringIO.new(STDIN.read) rescue nil
41
- return_error "No CSV data to validate" if !options[:schema] && source.nil?
37
+ def read_source(source)
38
+ if source.nil?
39
+ # If no source is present, try reading from stdin
40
+ if !$stdin.tty?
41
+ source = begin
42
+ StringIO.new($stdin.read)
43
+ rescue
44
+ nil
42
45
  end
43
- else
44
- # If the source isn't a URL, it's a file
45
- unless source =~ /^http(s)?/
46
- begin
47
- source = File.new( source )
48
- rescue Errno::ENOENT
49
- return_error "#{source} not found"
50
- end
46
+ return_error "No CSV data to validate" if !options[:schema] && source.nil?
47
+ end
48
+ else
49
+ # If the source isn't a URL, it's a file
50
+ unless /^http(s)?/.match?(source)
51
+ begin
52
+ source = File.new(source)
53
+ rescue Errno::ENOENT
54
+ return_error "#{source} not found"
51
55
  end
52
56
  end
53
-
54
- source
55
57
  end
56
58
 
57
- def get_schema(schema)
58
- begin
59
- schema = Csvlint::Schema.load_from_json(schema, false)
60
- rescue Csvlint::Csvw::MetadataError => e
61
- return_error "invalid metadata: #{e.message}#{" at " + e.path if e.path}"
62
- rescue OpenURI::HTTPError, Errno::ENOENT
63
- return_error "#{options[:schema]} not found"
64
- end
59
+ source
60
+ end
65
61
 
66
- if schema.class == Csvlint::Schema && schema.description == "malformed"
67
- return_error "invalid metadata: malformed JSON"
68
- end
62
+ def get_schema(schema)
63
+ begin
64
+ schema = Csvlint::Schema.load_from_uri(schema, false)
65
+ rescue Csvlint::Csvw::MetadataError => e
66
+ return_error "invalid metadata: #{e.message}#{" at " + e.path if e.path}"
67
+ rescue OpenURI::HTTPError, Errno::ENOENT
68
+ return_error "#{options[:schema]} not found"
69
+ end
69
70
 
70
- schema
71
+ if schema.instance_of?(Csvlint::Schema) && schema.description == "malformed"
72
+ return_error "invalid metadata: malformed JSON"
71
73
  end
72
74
 
73
- def fetch_schema_tables(schema, options)
74
- valid = true
75
+ schema
76
+ end
75
77
 
76
- unless schema.instance_of? Csvlint::Csvw::TableGroup
77
- return_error "No CSV data to validate."
78
- end
79
- schema.tables.keys.each do |source|
78
+ def fetch_schema_tables(schema, options)
79
+ valid = true
80
+
81
+ unless schema.instance_of? Csvlint::Csvw::TableGroup
82
+ return_error "No CSV data to validate."
83
+ end
84
+ schema.tables.keys.each do |source|
85
+ unless /^http(s)?/.match?(source)
80
86
  begin
81
- source = source.sub("file:","")
82
- source = File.new( source )
87
+ source = source.sub("file:", "")
88
+ source = File.new(source)
83
89
  rescue Errno::ENOENT
84
90
  return_error "#{source} not found"
85
- end unless source =~ /^http(s)?/
86
- valid &= validate_csv(source, schema, options[:dump_errors], nil, options[:werror])
87
- end
88
-
89
- exit 1 unless valid
90
- end
91
-
92
- def print_error(index, error, dump, color)
93
- location = ""
94
- location += error.row.to_s if error.row
95
- location += "#{error.row ? "," : ""}#{error.column.to_s}" if error.column
96
- if error.row || error.column
97
- location = "#{error.row ? "Row" : "Column"}: #{location}"
98
- end
99
- output_string = "#{index+1}. "
100
- if error.column && @schema && @schema.class == Csvlint::Schema
101
- if @schema.fields[error.column - 1] != nil
102
- output_string += "#{@schema.fields[error.column - 1].name}: "
103
91
  end
104
92
  end
105
- output_string += "#{error.type}"
106
- output_string += ". #{location}" unless location.empty?
107
- output_string += ". #{error.content}" if error.content
93
+ valid &= validate_csv(source, schema, options[:dump_errors], nil, options[:werror])
94
+ end
108
95
 
109
- if $stdout.tty?
110
- puts output_string.colorize(color)
111
- else
112
- puts output_string
113
- end
96
+ exit 1 unless valid
97
+ end
114
98
 
115
- if dump
116
- pp error
99
+ def print_error(index, error, dump, color)
100
+ location = ""
101
+ location += error.row.to_s if error.row
102
+ location += "#{error.row ? "," : ""}#{error.column}" if error.column
103
+ if error.row || error.column
104
+ location = "#{error.row ? "Row" : "Column"}: #{location}"
105
+ end
106
+ output_string = "#{index + 1}. "
107
+ if error.column && @schema && @schema.instance_of?(Csvlint::Schema)
108
+ if @schema.fields[error.column - 1] != nil
109
+ output_string += "#{@schema.fields[error.column - 1].name}: "
117
110
  end
118
111
  end
112
+ output_string += error.type.to_s
113
+ output_string += ". #{location}" unless location.empty?
114
+ output_string += ". #{error.content}" if error.content
119
115
 
120
- def print_errors(errors, dump)
121
- if errors.size > 0
122
- errors.each_with_index { |error, i| print_error(i, error, dump, :red) }
123
- end
116
+ puts Rainbow(output_string).color(color)
117
+
118
+ if dump
119
+ pp error
124
120
  end
121
+ end
125
122
 
126
- def return_error(message)
127
- if $stdout.tty?
128
- puts message.colorize(:red)
129
- else
130
- puts message
131
- end
132
- exit 1
123
+ def print_errors(errors, dump)
124
+ if errors.size > 0
125
+ errors.each_with_index { |error, i| print_error(i, error, dump, :red) }
133
126
  end
127
+ end
134
128
 
135
- def validate_csv(source, schema, dump, json, werror)
136
- @error_count = 0
129
+ def return_error(message)
130
+ puts Rainbow(message).red
131
+ exit 1
132
+ end
137
133
 
138
- if json === true
139
- validator = Csvlint::Validator.new( source, {}, schema )
140
- else
141
- validator = Csvlint::Validator.new( source, {}, schema, { lambda: report_lines } )
142
- end
134
+ def validate_csv(source, schema, dump, json, werror)
135
+ @error_count = 0
143
136
 
144
- if source.class == String
145
- csv = source
146
- elsif source.class == File
147
- csv = source.path
148
- else
149
- csv = "CSV"
150
- end
137
+ validator = if json === true
138
+ Csvlint::Validator.new(source, {}, schema)
139
+ else
140
+ Csvlint::Validator.new(source, {}, schema, {lambda: report_lines})
141
+ end
151
142
 
152
- if json === true
153
- json = {
154
- validation: {
155
- state: validator.valid? ? "valid" : "invalid",
156
- errors: validator.errors.map { |v| hashify(v) },
157
- warnings: validator.warnings.map { |v| hashify(v) },
158
- info: validator.info_messages.map { |v| hashify(v) },
159
- }
160
- }.to_json
161
- print json
162
- else
163
- if $stdout.tty?
164
- puts "\r\n#{csv} is #{validator.valid? ? "VALID".green : "INVALID".red}"
165
- else
166
- puts "\r\n#{csv} is #{validator.valid? ? "VALID" : "INVALID"}"
167
- end
168
- print_errors(validator.errors, dump)
169
- print_errors(validator.warnings, dump)
170
- end
171
-
172
- return false if werror && validator.warnings.size > 0
173
- return validator.valid?
143
+ csv = if source.instance_of?(String)
144
+ source
145
+ elsif source.instance_of?(File)
146
+ source.path
147
+ else
148
+ "CSV"
174
149
  end
175
150
 
176
- def hashify(error)
177
- h = {
178
- type: error.type,
179
- category: error.category,
180
- row: error.row,
181
- col: error.column
182
- }
183
-
184
- if error.column && @schema && @schema.class == Csvlint::Schema && @schema.fields[error.column - 1] != nil
185
- field = @schema.fields[error.column - 1]
186
- h[:header] = field.name
187
- h[:constraints] = Hash[field.constraints.map {|k,v| [k.underscore, v] }]
188
- end
151
+ if json === true
152
+ json = {
153
+ validation: {
154
+ state: validator.valid? ? "valid" : "invalid",
155
+ errors: validator.errors.map { |v| hashify(v) },
156
+ warnings: validator.warnings.map { |v| hashify(v) },
157
+ info: validator.info_messages.map { |v| hashify(v) }
158
+ }
159
+ }.to_json
160
+ print json
161
+ else
162
+ puts "\r\n#{csv} is #{validator.valid? ? Rainbow("VALID").green : Rainbow("INVALID").red}"
163
+ print_errors(validator.errors, dump)
164
+ print_errors(validator.warnings, dump)
165
+ end
166
+
167
+ return false if werror && validator.warnings.size > 0
168
+ validator.valid?
169
+ end
189
170
 
190
- h
171
+ def hashify(error)
172
+ h = {
173
+ type: error.type,
174
+ category: error.category,
175
+ row: error.row,
176
+ col: error.column
177
+ }
178
+
179
+ if error.column && @schema && @schema.instance_of?(Csvlint::Schema) && @schema.fields[error.column - 1] != nil
180
+ field = @schema.fields[error.column - 1]
181
+ h[:header] = field.name
182
+ h[:constraints] = field.constraints.map { |k, v| [k.underscore, v] }.to_h
191
183
  end
192
184
 
193
- def report_lines
194
- lambda do |row|
195
- new_errors = row.errors.count
196
- if new_errors > @error_count
197
- print "!".red
198
- else
199
- print ".".green
200
- end
201
- @error_count = new_errors
185
+ h
186
+ end
187
+
188
+ def report_lines
189
+ lambda do |row|
190
+ new_errors = row.errors.count
191
+ if new_errors > @error_count
192
+ print Rainbow("!").red
193
+ else
194
+ print Rainbow(".").green
202
195
  end
196
+ @error_count = new_errors
203
197
  end
204
-
198
+ end
205
199
  end
206
200
  end