gourami 0.4.0 → 1.3.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 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.