validated_object 2.3.1 → 2.3.3

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
2
  SHA256:
3
- metadata.gz: 03a54694b8208e1e788cd55ece182bacdbfa75f3389db8c0dcc8902df92d9822
4
- data.tar.gz: 03c58a85faa371f663201c0deb0e7ed8cb021f35aae8d3568e18f24e7db09c59
3
+ metadata.gz: '039b75227869c70e8767ab8e48b5f355d9e048f7b6a18cacec6ca679259f0097'
4
+ data.tar.gz: 7b16daca0ce8d9cb81af01ce6d6a984cadbc7ef36ee2b7a72b1ac67aed854415
5
5
  SHA512:
6
- metadata.gz: 9f3bf8d6e81b2359e60f830d493e0a4fed9997bc21fa21c33f838794300e1b97514999ab42fb478a5cd22562ec89aa0dcfddf3a2c127b6b804f9f5f34faaea11
7
- data.tar.gz: 5f05239f7150ffc10dc11544717298ec6500804ba4ab764cc8ba1dcb0bf1c6af7dfe59c927c829183902b575b2133ebc5278d2b1b9a3d8cd701dda8fd7a08f4d
6
+ metadata.gz: b7766bafdcb4e6d23890584bffac5c1ce7744d5166d29061a3247b2b377f52195f80531028c041db8a98f80a2b675411528804058e7fc7b008327de96c14e2e6
7
+ data.tar.gz: 007c20d017c7ceaf708d69f05f8c1fc19c1c262313550670f8ce1469df2641e4c78799cac90301e6cea94acfa9d7ad8489d6fba034a440b9e08ece2a0e37abb3
data/.rspec CHANGED
@@ -1,2 +1 @@
1
- --format documentation
2
1
  --color
data/.rubocop.yml ADDED
@@ -0,0 +1,14 @@
1
+ plugins:
2
+ - rubocop-rspec
3
+ - rubocop-rake
4
+
5
+ AllCops:
6
+ SuggestExtensions: true
7
+ DisplayCopNames: true
8
+ ExtraDetails: true
9
+ Color: true
10
+ FormatStyleGuide: true
11
+ DisabledByDefault: false
12
+
13
+ Lint/ConstantDefinitionInBlock:
14
+ Enabled: false
data/CLAUDE.md ADDED
@@ -0,0 +1,67 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Commands
6
+
7
+ ### Development Setup
8
+ - `bin/setup` - Install dependencies (runs `bundle install`)
9
+ - `bin/console` - Start an interactive console with the gem loaded
10
+
11
+ ### Testing and Quality
12
+ - `rake spec` or `bundle exec rspec` - Run the test suite
13
+ - `bundle exec rubocop` - Run code linting (with rubocop-rspec and rubocop-rake plugins)
14
+ - `rake` - Default task (runs specs)
15
+
16
+ ### Gem Development
17
+ - `bundle exec rake install` - Install gem locally for testing
18
+ - `bundle exec rake release` - Release new version (updates version, creates git tag, pushes to rubygems.org)
19
+
20
+ ## Architecture
21
+
22
+ This is a Ruby gem that provides self-validating Plain Old Ruby Objects using ActiveModel validations. The core architecture consists of:
23
+
24
+ ### Main Components
25
+
26
+ **ValidatedObject::Base** (`lib/validated_object.rb`):
27
+ - The main class that objects inherit from to gain validation capabilities
28
+ - Includes `ActiveModel::Validations` for standard Rails validation methods
29
+ - Automatically validates objects during instantiation via `initialize`
30
+ - Provides `check_validations!` method for explicit validation with clear error messages
31
+ - Contains a custom `TypeValidator` that supports type checking including arrays and pseudo-boolean validation
32
+
33
+ **ValidatedObject::SimplifiedApi** (`lib/validated_object/simplified_api.rb`):
34
+ - Provides convenience methods like `validates_attr` that combine `attr_reader` and `validates`
35
+ - Supports streamlined array element type validation with `[ElementType]` syntax
36
+ - Allows `validated` as synonym for `validates`
37
+
38
+ ### Key Features
39
+
40
+ - **Type Validation**: Custom `TypeValidator` supports class validation, pseudo-boolean (`Boolean` class), and array element type checking
41
+ - **Array Element Validation**: Two syntaxes supported:
42
+ - `validates_attr :tags, type: Array, element_type: String`
43
+ - `validates_attr :tags, type: [String]` (streamlined syntax)
44
+ - **Immutable Objects**: Uses `attr_reader` with instance variable setting to create read-only validated objects
45
+ - **Clear Error Messages**: Validation failures provide descriptive messages like "Birthday is a String, not a Date"
46
+
47
+ ### Validation Patterns
48
+
49
+ The gem follows a declarative pattern where classes define their validation rules upfront:
50
+
51
+ ```ruby
52
+ class Dog < ValidatedObject::Base
53
+ validates_attr :name, presence: true
54
+ validates_attr :birthday, type: Date, allow_nil: true
55
+ validates_attr :tags, type: [String], allow_nil: true
56
+ end
57
+ ```
58
+
59
+ Objects validate themselves during instantiation and raise `ArgumentError` with detailed messages if invalid.
60
+
61
+ ## Development Notes
62
+
63
+ - Requires Ruby 3.1+
64
+ - Uses RSpec for testing with color output enabled
65
+ - RuboCop configured with rspec and rake plugins
66
+ - Gem specification allows pushing to rubygems.org
67
+ - Demo script available at `script/demo.rb`
data/HISTORY.md CHANGED
@@ -1,2 +1,5 @@
1
+ # 2.3.2
2
+ - Add 'validated' as a synonym for 'validates' (and in the simplified API), so both can be used interchangeably.
3
+
1
4
  # 1.1.0
2
5
  Introduces the new Boolean pseudo-type.
data/README.md CHANGED
@@ -12,7 +12,7 @@ class Person < ValidatedObject::Base
12
12
  end
13
13
 
14
14
  # Instantiating it runs the validations.
15
- me = Person.new(name: 'Robb')
15
+ me = Person.new(name: 'Robert')
16
16
  you = Person.new(name: '') # => ArgumentError: "Name can't be blank"
17
17
  ```
18
18
 
@@ -82,6 +82,19 @@ validates :premium_membership, type: Boolean
82
82
  #...
83
83
  ```
84
84
 
85
+
86
+ ### Array element type validation
87
+
88
+ You can validate that an attribute is an array of a specific type using array syntax:
89
+
90
+ ```ruby
91
+ # Validate that names is an array of String objects.
92
+ validates_attr :names, type: [String]
93
+ ```
94
+
95
+ If the array contains any elements that are not of the specified type, validation will fail with a clear error message.
96
+
97
+
85
98
  ### Instantiating and automatically validating
86
99
 
87
100
  ```ruby
@@ -1,4 +1,4 @@
1
- require "active_support/concern"
1
+ require 'active_support/concern'
2
2
 
3
3
  # Enable a simplified API for the common case of
4
4
  # read-only ValidatedObjects.
@@ -10,9 +10,37 @@ module ValidatedObject
10
10
  # Simply delegate to `attr_reader` and `validates`.
11
11
  def validated_attr(attribute, *options)
12
12
  attr_reader attribute
13
+
13
14
  validates attribute, *options
14
15
  end
15
- end
16
16
 
17
+ # Allow 'validated' as a synonym for 'validates'.
18
+ def validated(*args, **kwargs, &block)
19
+ validates(*args, **kwargs, &block)
20
+ end
21
+
22
+ # Alias for validated_attr for compatibility with test usage.
23
+ def validates_attr(attribute, *options, **kwargs)
24
+ attr_reader attribute
25
+
26
+ if kwargs[:type]
27
+ type_val = kwargs.delete(:type)
28
+ element_type = kwargs.delete(:element_type)
29
+
30
+ # Parse Array[ElementType] syntax
31
+ if type_val.is_a?(Array) && type_val.length == 1 && type_val[0].is_a?(Class)
32
+ # This handles Array[Comment] syntax
33
+ element_type = type_val[0]
34
+ type_val = Array
35
+ end
36
+
37
+ opts = { type: { with: type_val } }
38
+ opts[:type][:element_type] = element_type if element_type
39
+ validates attribute, opts.merge(kwargs)
40
+ else
41
+ validates attribute, *options, **kwargs
42
+ end
43
+ end
44
+ end
17
45
  end
18
46
  end
@@ -1,6 +1,5 @@
1
- # typed: strict
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module ValidatedObject
5
- VERSION = '2.3.1'
4
+ VERSION = '2.3.3'
6
5
  end
@@ -1,12 +1,9 @@
1
- # typed: true
2
1
  # frozen_string_literal: true
3
2
 
4
3
  require 'active_model'
5
- require 'sorbet-runtime'
6
4
  require 'validated_object/version'
7
5
  require 'validated_object/simplified_api'
8
6
 
9
-
10
7
  module ValidatedObject
11
8
  # @abstract Subclass and add `attr_accessor` and validations
12
9
  # to create custom validating objects.
@@ -46,11 +43,8 @@ module ValidatedObject
46
43
  class Base
47
44
  include ActiveModel::Validations
48
45
  include SimplifiedApi
49
- extend T::Sig
50
-
51
- SymbolHash = T.type_alias { T::Hash[Symbol, T.untyped] }
52
46
 
53
- EMPTY_HASH = T.let({}.freeze, SymbolHash)
47
+ EMPTY_HASH = {}.freeze
54
48
 
55
49
  # A private class definition, not intended to
56
50
  # be used directly. Implements a pseudo-boolean class
@@ -66,15 +60,14 @@ module ValidatedObject
66
60
  #
67
61
  # @raise [ArgumentError] if the object is not valid at the
68
62
  # end of initialization or `attributes` is not a Hash.
69
- sig { params(attributes: SymbolHash).void }
70
63
  def initialize(attributes = EMPTY_HASH)
71
64
  set_instance_variables from_hash: attributes
72
65
  check_validations!
73
- nil
74
66
  end
75
67
 
76
68
  def validated_attr(attribute_name, **validation_options)
77
69
  attr_reader attribute_name
70
+
78
71
  validates attribute_name, validation_options
79
72
  end
80
73
 
@@ -82,7 +75,6 @@ module ValidatedObject
82
75
  #
83
76
  # @raise [ArgumentError] if any validations fail.
84
77
  # @return [ValidatedObject::Base] the receiver
85
- sig { returns(ValidatedObject::Base) }
86
78
  def check_validations!
87
79
  raise ArgumentError, errors.full_messages.join('; ') if invalid?
88
80
 
@@ -102,22 +94,22 @@ module ValidatedObject
102
94
  # validates :neutered, type: Boolean, allow_nil: true # Typed but optional
103
95
  # end
104
96
  class TypeValidator < ActiveModel::EachValidator
105
- extend T::Sig
106
-
107
97
  # @return [nil]
108
- sig do
109
- params(
110
- record: T.untyped,
111
- attribute: T.untyped,
112
- value: T.untyped
113
- )
114
- .void
115
- end
116
98
  def validate_each(record, attribute, value)
117
- validation_options = T.let(options, SymbolHash)
118
-
99
+ validation_options = options
119
100
  expected_class = validation_options[:with]
120
101
 
102
+ # Support type: Array, element_type: ElementType
103
+ if expected_class == Array && validation_options[:element_type]
104
+ return save_error(record, attribute, value, validation_options) unless value.is_a?(Array)
105
+
106
+ element_type = validation_options[:element_type]
107
+ unless value.all? { |el| el.is_a?(element_type) }
108
+ record.errors.add attribute, validation_options[:message] || "contains non-#{element_type} elements"
109
+ end
110
+ return
111
+ end
112
+
121
113
  return if pseudo_boolean?(expected_class, value) ||
122
114
  expected_class?(expected_class, value)
123
115
 
@@ -126,40 +118,39 @@ module ValidatedObject
126
118
 
127
119
  private
128
120
 
129
- sig { params(expected_class: T.untyped, value: T.untyped).returns(T.untyped) }
130
121
  def pseudo_boolean?(expected_class, value)
131
122
  expected_class == Boolean && boolean?(value)
132
123
  end
133
124
 
134
- sig { params(expected_class: T.untyped, value: T.untyped).returns(T.untyped) }
135
125
  def expected_class?(expected_class, value)
136
126
  value.is_a?(expected_class)
137
127
  end
138
128
 
139
- sig { params(value: T.untyped).returns(T.untyped) }
140
129
  def boolean?(value)
141
130
  value.is_a?(TrueClass) || value.is_a?(FalseClass)
142
131
  end
143
132
 
144
- sig do
145
- params(
146
- record: T.untyped,
147
- attribute: T.untyped,
148
- value: T.untyped,
149
- validation_options: SymbolHash
150
- )
151
- .void
152
- end
153
133
  def save_error(record, attribute, value, validation_options)
154
134
  record.errors.add attribute,
155
135
  validation_options[:message] || "is a #{value.class}, not a #{validation_options[:with]}"
156
136
  end
157
137
  end
158
138
 
139
+ # Register the TypeValidator with ActiveModel so `type:` validation option works
140
+ unless ActiveModel::Validations.const_defined?(:TypeValidator)
141
+ ActiveModel::Validations.const_set(:TypeValidator, TypeValidator)
142
+ end
143
+
144
+ # Allow 'validated' as a synonym for 'validates'
145
+ def self.validated(*args, **kwargs, &block)
146
+ validates(*args, **kwargs, &block)
147
+ end
148
+
159
149
  private
160
150
 
161
- sig { params(from_hash: SymbolHash).void }
162
151
  def set_instance_variables(from_hash:)
152
+ raise TypeError, "#{from_hash} is not a hash" unless from_hash.is_a?(Hash)
153
+
163
154
  from_hash.each do |variable_name, variable_value|
164
155
  # Test for the attribute reader
165
156
  send variable_name.to_sym
data/script/demo.rb CHANGED
@@ -1,9 +1,9 @@
1
- # typed: ignore
2
1
  require 'date'
3
2
  require 'validated_object'
4
3
 
5
4
  class Dog < ValidatedObject::Base
6
5
  attr_reader :name, :birthday
6
+
7
7
  validates :name, presence: true
8
8
  validates :birthday, type: Date, allow_nil: true
9
9
  end
@@ -15,4 +15,4 @@ maru = Dog.new(birthday: Date.today, name: 'Maru')
15
15
  puts maru.inspect
16
16
 
17
17
  hiro = Dog.new(birthday: 'today')
18
- puts hiro.inspect
18
+ puts hiro.inspect
@@ -19,11 +19,9 @@ Gem::Specification.new do |spec|
19
19
 
20
20
  # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
21
21
  # delete this section to allow pushing this gem to any host.
22
- if spec.respond_to?(:metadata)
23
- spec.metadata['allowed_push_host'] = 'https://rubygems.org'
24
- else
25
- raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.'
26
- end
22
+ raise 'RubyGems 2.0 or newer is required to protect against public gem pushes.' unless spec.respond_to?(:metadata)
23
+
24
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
27
25
 
28
26
  spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
29
27
  spec.bindir = 'exe'
@@ -32,8 +30,9 @@ Gem::Specification.new do |spec|
32
30
 
33
31
  spec.add_development_dependency 'rake', '>= 12.3.3'
34
32
  spec.add_development_dependency 'rspec', '>= 3.9.0'
35
- spec.add_development_dependency 'sorbet', '>= 0.5.5890'
33
+ spec.add_development_dependency 'rubocop', '>= 1.80.0'
34
+ spec.add_development_dependency 'rubocop-rake', '>= 0.0.0'
35
+ spec.add_development_dependency 'rubocop-rspec', '>= 0.0.0'
36
36
 
37
37
  spec.add_runtime_dependency 'activemodel', '>= 3.2.21'
38
- spec.add_runtime_dependency 'sorbet-runtime', '>= 0.5.5890'
39
38
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: validated_object
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.1
4
+ version: 2.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robb Shecter
@@ -38,47 +38,61 @@ dependencies:
38
38
  - !ruby/object:Gem::Version
39
39
  version: 3.9.0
40
40
  - !ruby/object:Gem::Dependency
41
- name: sorbet
41
+ name: rubocop
42
42
  requirement: !ruby/object:Gem::Requirement
43
43
  requirements:
44
44
  - - ">="
45
45
  - !ruby/object:Gem::Version
46
- version: 0.5.5890
46
+ version: 1.80.0
47
47
  type: :development
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - ">="
52
52
  - !ruby/object:Gem::Version
53
- version: 0.5.5890
53
+ version: 1.80.0
54
54
  - !ruby/object:Gem::Dependency
55
- name: activemodel
55
+ name: rubocop-rake
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - ">="
59
59
  - !ruby/object:Gem::Version
60
- version: 3.2.21
61
- type: :runtime
60
+ version: 0.0.0
61
+ type: :development
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
- version: 3.2.21
67
+ version: 0.0.0
68
68
  - !ruby/object:Gem::Dependency
69
- name: sorbet-runtime
69
+ name: rubocop-rspec
70
70
  requirement: !ruby/object:Gem::Requirement
71
71
  requirements:
72
72
  - - ">="
73
73
  - !ruby/object:Gem::Version
74
- version: 0.5.5890
74
+ version: 0.0.0
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 0.0.0
82
+ - !ruby/object:Gem::Dependency
83
+ name: activemodel
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 3.2.21
75
89
  type: :runtime
76
90
  prerelease: false
77
91
  version_requirements: !ruby/object:Gem::Requirement
78
92
  requirements:
79
93
  - - ">="
80
94
  - !ruby/object:Gem::Version
81
- version: 0.5.5890
95
+ version: 3.2.21
82
96
  description: A small wrapper around ActiveModel Validations.
83
97
  email:
84
98
  - robb@public.law
@@ -89,7 +103,9 @@ files:
89
103
  - ".github/workflows/ruby.yml"
90
104
  - ".gitignore"
91
105
  - ".rspec"
106
+ - ".rubocop.yml"
92
107
  - ".travis.yml"
108
+ - CLAUDE.md
93
109
  - Gemfile
94
110
  - HISTORY.md
95
111
  - LICENSE.txt
@@ -101,24 +117,6 @@ files:
101
117
  - lib/validated_object/simplified_api.rb
102
118
  - lib/validated_object/version.rb
103
119
  - script/demo.rb
104
- - sorbet/config
105
- - sorbet/rbi/gems/activemodel.rbi
106
- - sorbet/rbi/gems/activesupport.rbi
107
- - sorbet/rbi/gems/concurrent-ruby.rbi
108
- - sorbet/rbi/gems/i18n.rbi
109
- - sorbet/rbi/gems/rake.rbi
110
- - sorbet/rbi/gems/rspec-core.rbi
111
- - sorbet/rbi/gems/rspec-expectations.rbi
112
- - sorbet/rbi/gems/rspec-mocks.rbi
113
- - sorbet/rbi/gems/rspec-support.rbi
114
- - sorbet/rbi/gems/rspec.rbi
115
- - sorbet/rbi/gems/site_ruby.rbi
116
- - sorbet/rbi/gems/thread_safe.rbi
117
- - sorbet/rbi/gems/tzinfo.rbi
118
- - sorbet/rbi/sorbet-typed/lib/activemodel/all/activemodel.rbi
119
- - sorbet/rbi/sorbet-typed/lib/activesupport/>=6/activesupport.rbi
120
- - sorbet/rbi/sorbet-typed/lib/activesupport/all/activesupport.rbi
121
- - sorbet/rbi/sorbet-typed/lib/minitest/all/minitest.rbi
122
120
  - validated_object.gemspec
123
121
  homepage: https://github.com/public-law/validated_object
124
122
  licenses:
@@ -139,7 +137,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
139
137
  - !ruby/object:Gem::Version
140
138
  version: '0'
141
139
  requirements: []
142
- rubygems_version: 3.6.9
140
+ rubygems_version: 3.7.1
143
141
  specification_version: 4
144
142
  summary: Self-validating plain Ruby objects.
145
143
  test_files: []
data/sorbet/config DELETED
@@ -1,2 +0,0 @@
1
- --dir
2
- .