gourami 0.4.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 603c7eab7f67f882417c3d2c3972b8632e50d420
4
- data.tar.gz: d9ddbf73b7e29f40443d3f2ff421f434c66ee073
2
+ SHA256:
3
+ metadata.gz: 9aeeb89759c11dd7f9559824ccfd42bc6ab7755aa2d60f11b8a38e5b0db08902
4
+ data.tar.gz: 80c1968c495c2cff9277b4bd51c0340bd8381874dc864d53dab711091f61443a
5
5
  SHA512:
6
- metadata.gz: 2e98badb6ec80ba426d32da18e915944c7461e9154348efdc5c401cac9053224e4a82ed16ca78d89d81c6506cce4d6c940cf53e84e253e4ea87d1a7008634d1b
7
- data.tar.gz: dddd5426e7a1c497ccd0ff57c2536944d51077dd7317637fa3961943605afed939045047043017e2bdfe3bd2b8a8f22b93e11558a3f6e47206477b63cbceb57b
6
+ metadata.gz: 97af9a59317e79d499c95b7b05f31a5089e9f56c7b7970818c22b36482de075f646f212b12e41ad4b53d9be1e967c34b08b34e55c46a90e033151f46416208dc
7
+ data.tar.gz: 48979f3194878e519431c3a46deb3da5eea9dc624731483f89e8006370d84c30027445b4ac6b31f0ca91b1d669539ad79edf988c51a2d8b2f56f7877b1659cdd
data/README.md CHANGED
@@ -252,7 +252,11 @@ end
252
252
 
253
253
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
254
254
 
255
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
255
+ To install this gem onto your local machine, run `bundle exec rake install`.
256
+
257
+ To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
258
+
259
+ To add another gem owner to gourami gem `gem owner --add john.smith@example.com gourami`
256
260
 
257
261
  ## Contributing
258
262
 
data/Rakefile CHANGED
@@ -8,4 +8,18 @@ Rake::TestTask.new(:test) do |t|
8
8
  end
9
9
 
10
10
  task :spec => :test
11
+ namespace :test do
12
+ task :watch do |t, args|
13
+ require "filewatcher"
14
+
15
+ watcher = Filewatcher.new(["spec/", "lib/"], :every => true, :spinner => true, :immediate => true)
16
+ watcher.watch do |filename, event|
17
+ begin
18
+ Rake::Task[:test].execute(args)
19
+ rescue StandardError => error
20
+ puts "Error: #{error.message}"
21
+ end
22
+ end
23
+ end
24
+ end
11
25
  task :default => :test
@@ -21,8 +21,9 @@ Gem::Specification.new do |spec|
21
21
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
22
  spec.require_paths = ["lib"]
23
23
 
24
+ spec.add_development_dependency "activesupport", ">= 5.1.7"
25
+ spec.add_development_dependency "filewatcher", "~> 1.1.0"
26
+ spec.add_development_dependency "minitest", "~> 5.0"
24
27
  spec.add_development_dependency "pry", "~>0.10"
25
- spec.add_development_dependency "bundler", "~> 1.13"
26
28
  spec.add_development_dependency "rake", "~> 10.0"
27
- spec.add_development_dependency "minitest", "~> 5.0"
28
29
  end
@@ -2,6 +2,7 @@ module Gourami
2
2
  end
3
3
 
4
4
  require "gourami/error"
5
+ require "gourami/attribute_name_conflict_error"
5
6
  require "gourami/configuration_error"
6
7
  require "gourami/not_watching_changes_error"
7
8
  require "gourami/required_attribute_error"
@@ -0,0 +1,4 @@
1
+ module Gourami
2
+ class AttributeNameConflictError < Gourami::Error
3
+ end
4
+ end
@@ -2,6 +2,7 @@ module Gourami
2
2
  module Attributes
3
3
 
4
4
  module ClassMethods
5
+
5
6
  # Copy parent attributes to inheriting class.
6
7
  #
7
8
  # @param klass [Class]
@@ -20,11 +21,16 @@ module Gourami
20
21
  # @block default_block
21
22
  # If provided, the block will be applied to options as the :default
22
23
  def attribute(name, options = {}, &default_block)
24
+ base = self
23
25
  options = options.dup
24
26
  options[:default] = default_block if block_given?
25
27
 
26
28
  mixin = Module.new do |mixin|
27
29
  unless options[:skip_reader]
30
+ if !base.attributes.key?(name) && base.instance_methods.include?(name) && !options[:override_reader]
31
+ raise AttributeNameConflictError, "#{name} is already a method. To use the existing method, use `:skip_reader => true` option. To override the existing method, use `:override_reader => true` option."
32
+ end
33
+
28
34
  mixin.send(:define_method, :"#{name}") do
29
35
  value = instance_variable_get(:"@#{name}")
30
36
  default = options[:default]
@@ -11,10 +11,7 @@ module Gourami
11
11
  # @return [*]
12
12
  def setter_filter(attribute_name, value, options)
13
13
  type = options[:type]
14
- coercer_method_name = :"coerce_#{type}"
15
- if type
16
- value = send(coercer_method_name, value, options)
17
- end
14
+ value = send(:"coerce_#{type}", value, options) if type
18
15
 
19
16
  super(attribute_name, value, options)
20
17
  end
@@ -23,12 +20,17 @@ module Gourami
23
20
  #
24
21
  # @param value [Object]
25
22
  # @option allow_nil [Boolean] (true)
23
+ # @option nil_when_empty [Boolean] (true)
26
24
  # @option upcase [Boolean] (false)
27
25
  # @option strip [Boolean] (true)
28
26
  #
29
27
  # @return [String, nil]
30
28
  def coerce_string(value, options = {})
31
- return nil if value.nil? && options.fetch(:allow_nil, true)
29
+ if options.fetch(:allow_nil, true)
30
+ return if value.nil?
31
+ value = value.to_s
32
+ return if value.empty? && options.fetch(:nil_when_empty, false)
33
+ end
32
34
 
33
35
  value = value.to_s.dup.force_encoding(Encoding::UTF_8)
34
36
  value.strip! if options.fetch(:strip, true)
@@ -44,9 +46,9 @@ module Gourami
44
46
  #
45
47
  # @return [Boolean, nil]
46
48
  def coerce_boolean(value, options = {})
47
- return nil if options[:allow_nil] && (value.nil? || value == "")
49
+ return if options[:allow_nil] && (value.nil? || value == "")
48
50
  return false if value.to_s.strip == "false"
49
- !!value && !coerce_string(value).strip.empty?
51
+ !!value && !coerce_string(value, :allow_nil => false).strip.empty?
50
52
  end
51
53
 
52
54
  # Coerce the value into an Array.
@@ -63,31 +65,29 @@ module Gourami
63
65
  # @return [Array]
64
66
  # The coerced Array.
65
67
  def coerce_array(value, options = {})
68
+ return if options[:allow_nil] && value.nil?
69
+
66
70
  element_type = options[:element_type]
67
- value = value.values if value.kind_of?(Hash)
71
+ value = value.values if value.is_a?(Hash)
68
72
 
69
- if options[:split_by] && value.kind_of?(String)
73
+ if options[:split_by] && value.is_a?(String)
70
74
  value = value.split(options[:split_by]).map(&:strip)
71
75
  end
72
76
 
73
- if value.kind_of?(Array)
74
- if element_type
75
- if element_type.kind_of?(Hash)
76
- element_type_options = element_type
77
- element_type = element_type[:type]
78
- else
79
- element_type_options = {}
80
- end
81
-
82
- coercer_method_name = :"coerce_#{element_type}"
83
- value.map do |array_element|
84
- send(coercer_method_name, array_element, element_type_options)
85
- end
86
- else
87
- value
88
- end
77
+ return [] unless value.is_a?(Array)
78
+ return value unless element_type
79
+
80
+ if element_type.is_a?(Hash)
81
+ element_type_options = element_type
82
+ element_type = element_type[:type]
89
83
  else
90
- []
84
+ element_type_options = {}
85
+ end
86
+
87
+ coercer_method_name = :"coerce_#{element_type}"
88
+
89
+ value.map do |array_element|
90
+ send(coercer_method_name, array_element, element_type_options)
91
91
  end
92
92
  end
93
93
 
@@ -96,7 +96,7 @@ module Gourami
96
96
  # @param value [Object]
97
97
  #
98
98
  # @return [Float, nil]
99
- def coerce_float(value, options = {})
99
+ def coerce_float(value, _options = {})
100
100
  Float(value) rescue nil
101
101
  end
102
102
 
@@ -107,7 +107,8 @@ module Gourami
107
107
  #
108
108
  # @return [String, nil]
109
109
  def coerce_phone(value, options = {})
110
- value ? coerce_string(value).upcase.gsub(/[^+0-9A-Z]/,"") : nil
110
+ value = coerce_string(value, options)
111
+ value ? value.upcase.gsub(/[^+0-9A-Z]/, "") : nil
111
112
  end
112
113
 
113
114
  # Coerce the value into a Hash.
@@ -118,23 +119,39 @@ module Gourami
118
119
  # The type of the hash keys to coerce, no coersion if value is nil.
119
120
  # @option options :value_type [Symbol, Callable] (nil)
120
121
  # The type of the hash values to coerce, no coersion if value is nil.
122
+ # @option options :indifferent_access [Boolean] (false)
123
+ # When true, the resulting Hash will be an ActiveSupport::HashWithIndifferentAccess
121
124
  #
122
- # @return [Hash]
125
+ # @return [Hash, ActiveSupport::HashWithIndifferentAccess]
123
126
  # The coerced Hash.
124
127
  def coerce_hash(value, options = {})
128
+ return if options[:allow_nil] && value.nil?
129
+
125
130
  hash_key_type = options[:key_type]
126
131
  hash_value_type = options[:value_type]
127
- if value.kind_of?(Hash) || value.kind_of?(Sequel::Postgres::JSONHash)
128
- value.each_with_object({}) do |(key, value), coerced_hash|
129
- key_type = hash_key_type.respond_to?(:call) ? hash_key_type.call(key, value) : hash_key_type
130
- key = send("coerce_#{key_type}", key) if key_type
131
-
132
- value_type = hash_value_type.respond_to?(:call) ? hash_value_type.call(key, value) : hash_value_type
133
- value = send("coerce_#{value_type}", value) if value_type
134
- coerced_hash[key] = value
132
+
133
+ hash_class = options[:indifferent_access] ? ActiveSupport::HashWithIndifferentAccess : Hash
134
+ hash = hash_class.new
135
+
136
+ return hash unless value.is_a?(Hash) || (defined?(Sequel::Postgres::JSONHash) && value.is_a?(Sequel::Postgres::JSONHash))
137
+
138
+ value.each_with_object(hash) do |(key, value), coerced_hash|
139
+ key_type = hash_key_type.respond_to?(:call) ? hash_key_type.call(key, value) : hash_key_type
140
+ key = send(:"coerce_#{key_type}", key) if key_type
141
+
142
+ value_type = hash_value_type.respond_to?(:call) ? hash_value_type.call(key, value) : hash_value_type
143
+
144
+ # TODO: Refactor shared logic here and coerce_array to a method like `type, options = resolve_coercer_type_and_options`
145
+ if value_type.is_a?(Hash)
146
+ value_type_options = value_type
147
+ value_type = value_type[:type]
148
+ else
149
+ value_type_options = {}
135
150
  end
136
- else
137
- {}
151
+
152
+ value = send(:"coerce_#{value_type}", value, value_type_options) if value_type
153
+
154
+ coerced_hash[key] = value
138
155
  end
139
156
  end
140
157
 
@@ -145,13 +162,11 @@ module Gourami
145
162
  #
146
163
  # @return [Integer, nil]
147
164
  # An Integer if the value can be coerced or nil otherwise.
148
- def coerce_integer(value, options = {})
165
+ def coerce_integer(value, _options = {})
149
166
  value = value.to_s
150
- if value.match(/\A0|[1-9]\d*\z/)
151
- value.to_i
152
- else
153
- nil
154
- end
167
+ return unless value =~ /\A0|[1-9]\d*\z/
168
+
169
+ value.to_i
155
170
  end
156
171
 
157
172
  # Coerce the value into a Date.
@@ -163,7 +178,7 @@ module Gourami
163
178
  # A Date if the value can be coerced or nil otherwise.
164
179
  def coerce_date(value, options = {})
165
180
  value = coerce_string(value, options)
166
- return nil if value.nil?
181
+ return if value.nil?
167
182
  begin
168
183
  Date.strptime(value, "%Y-%m-%d")
169
184
  rescue ArgumentError
@@ -180,7 +195,7 @@ module Gourami
180
195
  # A Time if the value can be coerced or nil otherwise.
181
196
  def coerce_time(value, options = {})
182
197
  value = coerce_string(value, options)
183
- return nil if !value || value.empty?
198
+ return if !value || value.empty?
184
199
 
185
200
  begin
186
201
  Time.parse(value).utc
@@ -198,13 +213,13 @@ module Gourami
198
213
  # @return [Hash, nil]
199
214
  # The hash will contain the file String :filename and the File :tempfile,
200
215
  # or nil otherwise.
201
- def coerce_file(value, options = {})
202
- if value.kind_of?(Hash) && !value[:filename].to_s.empty?
203
- tempfile = value[:tempfile]
204
- if tempfile.kind_of?(File) || tempfile.kind_of?(Tempfile)
205
- value
206
- end
207
- end
216
+ def coerce_file(value, _options = {})
217
+ return unless value.is_a?(Hash) && !value[:filename].to_s.empty?
218
+
219
+ tempfile = value[:tempfile]
220
+ return unless tempfile.is_a?(File) || tempfile.is_a?(Tempfile)
221
+
222
+ value
208
223
  end
209
224
 
210
225
  end
@@ -91,6 +91,28 @@ module Gourami
91
91
  resource_errors.values.flat_map(&:values).map(&:values).flatten.any?
92
92
  end
93
93
 
94
+ # Replace the existing resource errors with the provided errors Hash.
95
+ #
96
+ # @param new_resource_errors [Hash<Symbol, Hash<Symbol, Hash<Symbol, Array>>>]
97
+ #
98
+ # @return [Hash<Symbol, Hash<Symbol, Hash<Symbol, Array>>>]
99
+ def clear_and_set_resource_errors(new_resource_errors)
100
+ new_resource_errors = new_resource_errors.dup
101
+ resource_errors.clear
102
+ resource_errors.merge!(new_resource_errors)
103
+
104
+ resource_errors
105
+ end
106
+
107
+ def handle_validation_error(error)
108
+ super(error)
109
+ clear_and_set_resource_errors(error.resource_errors) unless error.resource_errors.nil?
110
+ end
111
+
112
+ def raise_validate_errors
113
+ raise ValidationError.new(errors, resource_errors)
114
+ end
115
+
94
116
  end
95
117
  end
96
118
  end
@@ -9,20 +9,39 @@ module Gourami
9
9
  end
10
10
  end
11
11
 
12
+ def self.stringify_resource_errors(resource_errors)
13
+ [].tap do |array|
14
+ resource_errors.each do |resource_namespace, resource_namespace_errors|
15
+ resource_namespace_errors.each do |resource_uid, resource_uid_errors|
16
+ resource_uid_errors.each do |attribute_name, error|
17
+ array.push("#{resource_namespace}:#{resource_uid}:#{attribute_name}: #{error}")
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+
12
24
  # !@attribute [r] errors
13
25
  # @return [Hash<Symbol, Array>]
14
26
  attr_reader :errors
15
27
 
28
+ # !@attribute [r] resource_errors
29
+ # @return [Hash<Symbol, Hash<Symbol, Hash<Symbol, Array>>>]
30
+ attr_reader :resource_errors
31
+
16
32
  # Initialize the Gourami::ValidationError.
17
33
  #
18
34
  # @param errors [Hash<Symbol, Array>]
19
- def initialize(errors)
35
+ # @param resource_errors [Hash<Symbol, Hash<Symbol, Hash<Symbol, Array>>>]
36
+ def initialize(errors, resource_errors = {})
37
+ @resource_errors = resource_errors
20
38
  @errors = errors
39
+
21
40
  super(message)
22
41
  end
23
42
 
24
43
  def message
25
- @message ||= "Validation failed with errors: #{stringify_errors.join("\n")}"
44
+ @message ||= stringify_all_errors
26
45
  end
27
46
 
28
47
  private
@@ -31,5 +50,16 @@ module Gourami
31
50
  ValidationError.stringify_errors(errors)
32
51
  end
33
52
 
53
+ def stringify_resource_errors
54
+ ValidationError.stringify_resource_errors(resource_errors)
55
+ end
56
+
57
+ def stringify_all_errors
58
+ messages = []
59
+ messages << "Validation failed with errors: #{stringify_errors.join("\n")}" unless errors.nil? || errors.empty?
60
+ messages << "Validation failed with resource errors: #{stringify_resource_errors.join("\n")}" unless resource_errors.nil? || resource_errors.empty?
61
+ messages.join("\n")
62
+ end
63
+
34
64
  end
35
65
  end
@@ -15,11 +15,16 @@ module Gourami
15
15
  # @raise [Gourami::ValidationError]
16
16
  def perform!
17
17
  if valid?
18
- returned = perform
18
+ begin
19
+ returned = perform
20
+ rescue Gourami::ValidationError => error
21
+ handle_validation_error(error)
22
+ raise
23
+ end
19
24
  end
20
25
 
21
26
  if any_errors?
22
- raise ValidationError.new(errors)
27
+ raise_validate_errors
23
28
  end
24
29
 
25
30
  returned
@@ -46,9 +51,9 @@ module Gourami
46
51
 
47
52
  # Replace the existing errors with the provided errors Hash.
48
53
  #
49
- # @param new_errors [Hash<Symbol, nil>, Array<Symbol, String>]
54
+ # @param new_errors Hash<Symbol, Array>
50
55
  #
51
- # @return [Hash<Symbol, nil>, Array<Symbol, String>]
56
+ # @return Hash<Symbol, Array>
52
57
  def clear_and_set_errors(new_errors)
53
58
  new_errors = new_errors.dup
54
59
  errors.clear
@@ -57,6 +62,14 @@ module Gourami
57
62
  errors
58
63
  end
59
64
 
65
+ def raise_validate_errors
66
+ raise ValidationError.new(errors)
67
+ end
68
+
69
+ def handle_validation_error(error)
70
+ clear_and_set_errors(error.errors) unless error.errors.nil?
71
+ end
72
+
60
73
  # Return true if there given attribute has any errors.
61
74
  def attribute_has_errors?(attribute_name)
62
75
  errors[attribute_name.to_sym].any?
@@ -1,3 +1,3 @@
1
1
  module Gourami
2
- VERSION = "0.4.0".freeze
2
+ VERSION = "1.3.0".freeze
3
3
  end
metadata CHANGED
@@ -1,71 +1,85 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gourami
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - TSMMark
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-02-22 00:00:00.000000000 Z
11
+ date: 2020-06-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: pry
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 5.1.7
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 5.1.7
27
+ - !ruby/object:Gem::Dependency
28
+ name: filewatcher
15
29
  requirement: !ruby/object:Gem::Requirement
16
30
  requirements:
17
31
  - - "~>"
18
32
  - !ruby/object:Gem::Version
19
- version: '0.10'
33
+ version: 1.1.0
20
34
  type: :development
21
35
  prerelease: false
22
36
  version_requirements: !ruby/object:Gem::Requirement
23
37
  requirements:
24
38
  - - "~>"
25
39
  - !ruby/object:Gem::Version
26
- version: '0.10'
40
+ version: 1.1.0
27
41
  - !ruby/object:Gem::Dependency
28
- name: bundler
42
+ name: minitest
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
45
  - - "~>"
32
46
  - !ruby/object:Gem::Version
33
- version: '1.13'
47
+ version: '5.0'
34
48
  type: :development
35
49
  prerelease: false
36
50
  version_requirements: !ruby/object:Gem::Requirement
37
51
  requirements:
38
52
  - - "~>"
39
53
  - !ruby/object:Gem::Version
40
- version: '1.13'
54
+ version: '5.0'
41
55
  - !ruby/object:Gem::Dependency
42
- name: rake
56
+ name: pry
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
59
  - - "~>"
46
60
  - !ruby/object:Gem::Version
47
- version: '10.0'
61
+ version: '0.10'
48
62
  type: :development
49
63
  prerelease: false
50
64
  version_requirements: !ruby/object:Gem::Requirement
51
65
  requirements:
52
66
  - - "~>"
53
67
  - !ruby/object:Gem::Version
54
- version: '10.0'
68
+ version: '0.10'
55
69
  - !ruby/object:Gem::Dependency
56
- name: minitest
70
+ name: rake
57
71
  requirement: !ruby/object:Gem::Requirement
58
72
  requirements:
59
73
  - - "~>"
60
74
  - !ruby/object:Gem::Version
61
- version: '5.0'
75
+ version: '10.0'
62
76
  type: :development
63
77
  prerelease: false
64
78
  version_requirements: !ruby/object:Gem::Requirement
65
79
  requirements:
66
80
  - - "~>"
67
81
  - !ruby/object:Gem::Version
68
- version: '5.0'
82
+ version: '10.0'
69
83
  description: Create Plain Old Ruby Objects that take attributes, validate them, and
70
84
  perform an action.
71
85
  email:
@@ -85,6 +99,7 @@ files:
85
99
  - bin/setup
86
100
  - gourami.gemspec
87
101
  - lib/gourami.rb
102
+ - lib/gourami/attribute_name_conflict_error.rb
88
103
  - lib/gourami/attributes.rb
89
104
  - lib/gourami/coercer.rb
90
105
  - lib/gourami/configuration_error.rb
@@ -118,8 +133,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
118
133
  - !ruby/object:Gem::Version
119
134
  version: '0'
120
135
  requirements: []
121
- rubyforge_project:
122
- rubygems_version: 2.4.6
136
+ rubygems_version: 3.0.4
123
137
  signing_key:
124
138
  specification_version: 4
125
139
  summary: Keep your Routes, Controllers and Models thin.