parxer 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.gitignore +14 -0
  4. data/.hound.yml +4 -0
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +1038 -0
  7. data/.ruby-version +1 -0
  8. data/.travis.yml +12 -0
  9. data/CHANGELOG.md +13 -0
  10. data/Gemfile +4 -0
  11. data/Guardfile +5 -0
  12. data/LICENSE.txt +21 -0
  13. data/README.md +140 -0
  14. data/Rakefile +5 -0
  15. data/bin/console +14 -0
  16. data/bin/setup +8 -0
  17. data/docs/images/parxer-response.png +0 -0
  18. data/docs/images/superheroes-xls.png +0 -0
  19. data/lib/parxer.rb +10 -0
  20. data/lib/parxer/collections/attributes.rb +16 -0
  21. data/lib/parxer/collections/callbacks.rb +22 -0
  22. data/lib/parxer/collections/row_errors.rb +19 -0
  23. data/lib/parxer/collections/validators.rb +34 -0
  24. data/lib/parxer/dsl/dsl.rb +92 -0
  25. data/lib/parxer/formatters/base_formatter.rb +39 -0
  26. data/lib/parxer/formatters/boolean_formatter.rb +14 -0
  27. data/lib/parxer/formatters/custom_formatter.rb +13 -0
  28. data/lib/parxer/formatters/number_formatter.rb +25 -0
  29. data/lib/parxer/formatters/rut_formatter.rb +33 -0
  30. data/lib/parxer/formatters/string_formatter.rb +9 -0
  31. data/lib/parxer/parsers/base_parser.rb +80 -0
  32. data/lib/parxer/parsers/concerns/attributes.rb +25 -0
  33. data/lib/parxer/parsers/concerns/callback.rb +24 -0
  34. data/lib/parxer/parsers/concerns/config.rb +23 -0
  35. data/lib/parxer/parsers/concerns/formatter.rb +14 -0
  36. data/lib/parxer/parsers/concerns/validator.rb +75 -0
  37. data/lib/parxer/parsers/csv_parser.rb +13 -0
  38. data/lib/parxer/parsers/xls_parser.rb +21 -0
  39. data/lib/parxer/utils/context.rb +26 -0
  40. data/lib/parxer/utils/errors.rb +10 -0
  41. data/lib/parxer/utils/formatter_builder.rb +16 -0
  42. data/lib/parxer/utils/inherited_resource.rb +35 -0
  43. data/lib/parxer/utils/row_builder.rb +22 -0
  44. data/lib/parxer/validators/base_validator.rb +22 -0
  45. data/lib/parxer/validators/boolean_validator.rb +13 -0
  46. data/lib/parxer/validators/columns_validator.rb +19 -0
  47. data/lib/parxer/validators/custom_validator.rb +17 -0
  48. data/lib/parxer/validators/datetime_validator.rb +54 -0
  49. data/lib/parxer/validators/email_validator.rb +14 -0
  50. data/lib/parxer/validators/file_format_validator.rb +18 -0
  51. data/lib/parxer/validators/file_presence_validator.rb +9 -0
  52. data/lib/parxer/validators/inclusion_validator.rb +26 -0
  53. data/lib/parxer/validators/number_validator.rb +63 -0
  54. data/lib/parxer/validators/presence_validator.rb +9 -0
  55. data/lib/parxer/validators/rows_count_validator.rb +9 -0
  56. data/lib/parxer/validators/rut_validator.rb +32 -0
  57. data/lib/parxer/validators/url_validator.rb +11 -0
  58. data/lib/parxer/values/attribute.rb +19 -0
  59. data/lib/parxer/values/callback.rb +22 -0
  60. data/lib/parxer/values/row.rb +17 -0
  61. data/lib/parxer/version.rb +3 -0
  62. data/parxer.gemspec +32 -0
  63. metadata +247 -0
@@ -0,0 +1 @@
1
+ 2.3
@@ -0,0 +1,12 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.3.1
4
+ before_install: gem install bundler -v 1.12.5
5
+ deploy:
6
+ provider: rubygems
7
+ api_key:
8
+ secure: wKW5LVpI/tvuXAJjWc3P6g9K1qlIphBm5xNaSZVVm8C3o7MMypsPLX7+U3Bhz23mgkyBI5JSUx46a7fNm2xeKdXKZ5g5i72zpxJ0g7NbD3kTg5iZdYIJw75qQKoMMdYEAwZa6ojVtFQV8r9kcC/nKrULruy7t2wC9eIJ4WeLo7IsU5McdAcT8c177p8YdS/Yh+V0wNu62rto8FByz9yvgyzXYAmKQ1Dni9xMttvC2bTAtnBO9/ghUQl0qNbqpJD7QZAockomB8uPqhcIjAdg5KNVsa7HZCvPXKn453oWVXFsXoZqFTMftU/cknbf+Gip0+KVMUHcW1Dqq5PAvEiTp46KtJEFWLKwMPl0QSgN3/5DM3zbLQIXFGcJkbrMno1BUlHdUVB6TZ9B7Owx37khOw1TEdvf1ROuV6qCn266hs7xxKdLMpD3keD6ONYzyxFBOXGsuSAin5BvRc9LwR44PRdFgHH7GvpgxX1ajujmNH2cfdabmsLsYEqee4ZIA2K7ZybLv1SvMEvfUau1O3Ahg/1xgHHPB0iYfu0CdqAUquzXcpyX1hce1ANqljCVBG8YhOfunNxTJ43vnE6L02PxOqQMah3rE47oeFyiGHokKNJg9NJwpTlXgucdOUogtoSXoYBGUA81TdqaJBcD2QOVAZlguuNLiXcPVwFyGTjsL54=
9
+ gem: parxer
10
+ on:
11
+ tags: true
12
+ repo: platanus/parxer
@@ -0,0 +1,13 @@
1
+ # Change Log
2
+ All notable changes to this project will be documented in this file.
3
+ This project adheres to [Semantic Versioning](http://semver.org/).
4
+
5
+ ### v0.1.1
6
+
7
+ ##### Fixed
8
+
9
+ * Travis config
10
+
11
+ ### v0.1.0
12
+
13
+ * Initial release.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in parxer.gemspec
4
+ gemspec
@@ -0,0 +1,5 @@
1
+ guard :rspec, cmd: 'bundle exec rspec' do
2
+ watch(%r{^spec/.+_spec\.rb$})
3
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
4
+ watch('spec/spec_helper.rb') { "spec" }
5
+ end
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright 2017 Platanus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,140 @@
1
+ # Parxer
2
+
3
+ ruby gem to parse data from different source types
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ $ gem install parxer
9
+ ```
10
+
11
+ Or add to your Gemfile:
12
+
13
+ ```ruby
14
+ gem "parxer"
15
+ ```
16
+
17
+ ```bash
18
+ bundle install
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ Imagine you have an `xls` file like this:
24
+
25
+ <img src="./docs/images/superheroes-xls.png" />
26
+
27
+ To parse this file you need to:
28
+
29
+ ### 1 - Define your Parser using the DSL:
30
+
31
+ ```ruby
32
+ class SuperheroesParser < Parxer::XlsParser
33
+ validate_file(:rows_count, max: 50) # Define file validators
34
+
35
+ column(:name, name: "Name") do # Map column names to attributes
36
+ validate(:presence) # Add column validator
37
+ format_with do
38
+ value.upcase # Define custom formatters
39
+ end
40
+ end
41
+
42
+ column(:real_name, name: "Real Name")
43
+
44
+ column(:super_power, name: "Super Power") do
45
+ validate(:presence)
46
+ end
47
+
48
+ column(:publisher, name: "Publisher") do
49
+ validate(:inclusion, in: ["Marvel", "DC"])
50
+ end
51
+
52
+ column(:age, name: "Alive") do
53
+ validate(:presence)
54
+ validate(:number)
55
+ format_with(:number, integer: true) # Use Parxer's formatter
56
+ end
57
+
58
+ column(:is_alive, name: "Alive", format: :boolean) do
59
+ validate(:presence)
60
+ end
61
+
62
+ # Define validators to run after attribute validators
63
+ validate_row(:odd_chars, if_valid: [:name, :real_name]) do
64
+ (row.name + row.real_name).delete(" ").size.odd?
65
+ end
66
+
67
+ # Define callbacks
68
+ after_parse_row do
69
+ row.add_attribute(:full_name) # Add attributes dynamically
70
+ row.full_name = "#{row.name} (#{row.real_name})" unless row.errors?
71
+ end
72
+ end
73
+ ```
74
+
75
+ ### 2 - Initialize your parser:
76
+
77
+ ```ruby
78
+ parser = SuperheroesParser.new
79
+ ```
80
+
81
+ ### 3 - Use your parser:
82
+
83
+ ```ruby
84
+ result = parser.run("/some_path/superhero.xls"); #=> #<Enumerator: ...>
85
+ ```
86
+
87
+ Now, if you iterate through each row of the enumerator you will get something like this:
88
+
89
+ <img src="./docs/images/parxer-response.png" />
90
+
91
+ As you can see...
92
+
93
+ 1. Attributes like `name` or `is_alive` have been formatted.
94
+ 2. Errors in the rows are accessible through the `errors` attribute.
95
+ 3. `idx` attribute is the row number.
96
+ 4. the custom `full_name` attribute was added as a part of the response.
97
+
98
+ Some useful methods you can use are:
99
+
100
+ **In parser context:**
101
+
102
+ - `parser.valid_file?`
103
+ - `parser.file_error`
104
+ - `parser.run`
105
+
106
+ **In row (`row = result.next`) context:**
107
+
108
+ - `row.errors?`
109
+ - `row.errors`
110
+ - `row.attribute_error?(:some_attribute_name)`
111
+
112
+ ## Testing
113
+
114
+ To run the specs you need to execute, **in the root path of the gem**, the following command:
115
+
116
+ ```bash
117
+ bundle exec guard
118
+ ```
119
+
120
+ You need to put **all your tests** in the `/my_gem/spec/` directory.
121
+
122
+ ## Contributing
123
+
124
+ 1. Fork it
125
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
126
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
127
+ 4. Push to the branch (`git push origin my-new-feature`)
128
+ 5. Create new Pull Request
129
+
130
+ ## Credits
131
+
132
+ Thank you [contributors](https://github.com/platanus/parxer/graphs/contributors)!
133
+
134
+ <img src="http://platan.us/gravatar_with_text.png" alt="Platanus" width="250"/>
135
+
136
+ Parxer is maintained by [platanus](http://platan.us).
137
+
138
+ ## License
139
+
140
+ Parxer is © 2017 platanus, spa. It is free software and may be redistributed under the terms specified in the LICENSE file.
@@ -0,0 +1,5 @@
1
+ require 'rspec/core/rake_task'
2
+ require "bundler/gem_tasks"
3
+
4
+ task default: :spec
5
+ RSpec::Core::RakeTask.new
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "parxer"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,10 @@
1
+ require "parxer/version"
2
+ require "require_all"
3
+ require "active_support/all"
4
+ require "roo"
5
+ require "roo-xls"
6
+
7
+ require_rel "parxer"
8
+
9
+ module Parxer
10
+ end
@@ -0,0 +1,16 @@
1
+ module Parxer
2
+ class Attributes < Array
3
+ def add_attribute(id, name: nil)
4
+ if find_attribute(id)
5
+ raise Parxer::AttributesError.new("trying to add attribute with existent id")
6
+ end
7
+
8
+ self << Parxer::Attribute.new(id, name: name)
9
+ last
10
+ end
11
+
12
+ def find_attribute(attribute_id)
13
+ find { |attribute| attribute.id == attribute_id.to_sym }
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,22 @@
1
+ module Parxer
2
+ class Callbacks < Array
3
+ CALLBACK_TYPES = %i{after_parse_row}
4
+
5
+ def add_callback(type, action, config = {})
6
+ if !CALLBACK_TYPES.include?(type.to_sym)
7
+ raise Parxer::CallbacksError.new("invalid '#{type}' callback type")
8
+ end
9
+
10
+ if !action.is_a?(Proc) && !action.is_a?(Symbol)
11
+ raise Parxer::CallbacksError.new("action param must by a Proc or symbol method name")
12
+ end
13
+
14
+ self << Parxer::Callback.new(type: type, action: action, config: config || {})
15
+ last
16
+ end
17
+
18
+ def by_type(type)
19
+ select { |callback| callback.type == type.to_sym }
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ module Parxer
2
+ class RowErrors < Hash
3
+ def add_error(attribute_name, error)
4
+ self[attribute_name.to_sym] = error
5
+ end
6
+
7
+ def attribute_error?(attribute_name)
8
+ !!attribute_error(attribute_name)
9
+ end
10
+
11
+ def attribute_error(attribute_name)
12
+ self[attribute_name.to_sym]
13
+ end
14
+
15
+ def errors?
16
+ any?
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,34 @@
1
+ module Parxer
2
+ class Validators < Array
3
+ def add_validator(validator_name, config = {}, &block)
4
+ validator = validator_instance(validator_name, config, &block)
5
+
6
+ if find_validator(validator.id)
7
+ raise Parxer::ValidatorError.new("trying to add validator with existent id")
8
+ end
9
+
10
+ self << validator
11
+ last
12
+ end
13
+
14
+ private
15
+
16
+ def find_validator(validator_id)
17
+ find { |validator| validator.id.to_sym == validator_id.to_sym }
18
+ end
19
+
20
+ def validator_instance(validator_name, config = {}, &block)
21
+ config[:id] = validator_name
22
+ validator_class = infer_validator_class(validator_name)
23
+ config[:condition_proc] = block if validator_class == Parxer::Validator::Custom
24
+ validator_class.new(config)
25
+ end
26
+
27
+ def infer_validator_class(validator_name)
28
+ return Parxer::Validator::Custom if validator_name.blank?
29
+ "Parxer::Validator::#{validator_name.to_s.camelize}".constantize
30
+ rescue NameError
31
+ Parxer::Validator::Custom
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,92 @@
1
+ module Parxer
2
+ module Dsl
3
+ extend ActiveSupport::Concern
4
+
5
+ class_methods do
6
+ def in_context
7
+ current_method = caller_locations(1, 1)[0].label.to_sym
8
+ validate_context!(current_method)
9
+ current_context << current_method
10
+ yield
11
+ ensure
12
+ current_context.pop
13
+ end
14
+
15
+ def validate_context!(current_method)
16
+ dependencies = ctx_dependencies_map[current_method]
17
+
18
+ if current_context != dependencies
19
+ msg = if dependencies.any?
20
+ "'#{current_method}' needs to run inside '#{dependencies.last}' block"
21
+ else
22
+ "'#{current_method}' can't run inside '#{current_context.last}' block"
23
+ end
24
+
25
+ raise Parxer::DslError.new(msg)
26
+ end
27
+ end
28
+
29
+ def method_aliases
30
+ @method_aliases ||= {}
31
+ end
32
+
33
+ def ctx_dependencies_map
34
+ {
35
+ add_parser_option: [],
36
+ validate_file: [],
37
+ validate_row: [],
38
+ column: [],
39
+ after_parse_row: [],
40
+ validate: [:column],
41
+ format_with: [:column]
42
+ }
43
+ end
44
+
45
+ def current_context
46
+ @current_context ||= []
47
+ end
48
+
49
+ def column(id, config, &block)
50
+ in_context do
51
+ formatter_name = config.delete(:format)
52
+ @current_attr = attributes.add_attribute(id, name: config.delete(:name))
53
+ format_with(formatter_name) if formatter_name
54
+ block&.call
55
+ end
56
+ ensure
57
+ @current_attr = nil
58
+ end
59
+
60
+ def validate(validator_name, config = {}, &block)
61
+ in_context { @current_attr.add_validator(validator_name, config, &block) }
62
+ end
63
+
64
+ def validate_file(validator_name, config = {}, &block)
65
+ in_context { add_validator(validator_name, config, &block) }
66
+ end
67
+
68
+ def validate_row(validator_name, config = {}, &block)
69
+ in_context { add_row_validator(validator_name, config, &block) }
70
+ end
71
+
72
+ def format_with(*params, &block)
73
+ in_context do
74
+ formatter_name = !params.first.is_a?(Hash) ? params.first : :custom
75
+ config = (params.first.is_a?(Hash) ? params.first : params.second) || {}
76
+ @current_attr.add_formatter(formatter_name, config, &block)
77
+ end
78
+ end
79
+
80
+ def after_parse_row(callback_method = nil, &block)
81
+ in_context do
82
+ action = callback_method || block
83
+ parser_callbacks.add_callback(:after_parse_row, action)
84
+ end
85
+ end
86
+
87
+ def add_parser_option(key, value)
88
+ in_context { add_config_option(key, value) }
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,39 @@
1
+ module Parxer
2
+ module Formatter
3
+ class Base
4
+ include Parxer::Context
5
+
6
+ attr_reader :config
7
+
8
+ def initialize(config = {})
9
+ @context = config.delete(:context)
10
+ @config = config
11
+ end
12
+
13
+ def apply
14
+ v = context.value.to_s
15
+
16
+ if v.blank?
17
+ return default_value if default_value?
18
+ return nil
19
+ end
20
+
21
+ format_value(v)
22
+ end
23
+
24
+ def format_value(_v)
25
+ raise Parxer::FormatterError.new("'format_value' method not implemented")
26
+ end
27
+
28
+ private
29
+
30
+ def default_value?
31
+ !!default_value
32
+ end
33
+
34
+ def default_value
35
+ config[:default]
36
+ end
37
+ end
38
+ end
39
+ end