expectant 0.3.0 → 0.3.2

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.
data/lib/expectant/dsl.rb CHANGED
@@ -1,119 +1,132 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "utils"
4
- require_relative "schema"
5
- require_relative "bound_schema"
6
-
7
- module Expectant
8
- module DSL
9
- def self.included(base)
10
- base.extend(ClassMethods)
11
- end
12
-
13
- module ClassMethods
14
- def self.extended(base)
15
- base.class_eval do
16
- @_expectant_schemas = {}
17
- end
18
- end
19
-
20
- def inherited(sub)
21
- return unless instance_variable_defined?(:@_expectant_schemas)
22
- return if @_expectant_schemas.empty?
23
-
24
- # Deep copy each schema (fields + validators)
25
- parent_schemas = @_expectant_schemas
26
- duped = parent_schemas.transform_values { |schema| schema.duplicate }
27
-
28
- sub.instance_variable_set(:@_expectant_schemas, duped)
29
- end
30
-
31
- # Define a new expectation schema
32
- # Options:
33
- # collision: :error|:force -> method name collision policy for dynamic definitions
34
- # singular: string|symbol -> control singular name for the schema
35
- def expects(schema_name, collision: :error, singular: nil)
36
- field_method_name = Utils.singularize(schema_name.to_sym)
37
- if !singular.nil?
38
- if singular.is_a?(String) || singular.is_a?(Symbol)
39
- field_method_name = singular.to_sym
40
- else
41
- raise ConfigurationError, "Invalid singular option: #{singular.inspect}"
42
- end
43
- end
44
- schema = schema_name.to_sym
45
-
46
- if @_expectant_schemas.key?(schema)
47
- raise SchemaError, "Schema :#{schema} already defined"
48
- else
49
- create_schema(schema, collision: collision, field_method_name: field_method_name)
50
- end
51
-
52
- self
53
- end
54
-
55
- def reset_inherited_expectations!
56
- @_expectant_schemas = {}
57
- end
58
-
59
- private
60
-
61
- def create_schema(schema_name, collision: :error, field_method_name: nil)
62
- @_expectant_schemas[schema_name] = Schema.new(schema_name)
63
-
64
- # Dynamically define the field definition method
65
- # (e.g. input for :inputs, datum for :data)
66
- Utils.define_with_collision_policy(singleton_class, field_method_name, collision: collision) do
67
- define_singleton_method(field_method_name) do |name, **options|
68
- expectation = Expectation.new(name, **options)
69
- @_expectant_schemas[schema_name].add_field(expectation)
70
- expectation
71
- end
72
- end
73
-
74
- # Reset a schema
75
- reset_method_name = "reset_#{schema_name}!"
76
- Utils.define_with_collision_policy(singleton_class, reset_method_name, collision: collision) do
77
- define_singleton_method(reset_method_name) do
78
- @_expectant_schemas[schema_name].reset!
79
- end
80
- end
81
-
82
- # Define validators
83
- method_name = Utils.validator_method_name(field_method_name, Expectant.configuration)
84
- Utils.define_with_collision_policy(singleton_class, method_name, collision: collision) do
85
- define_singleton_method(method_name) do |*field_names, &block|
86
- @_expectant_schemas[schema_name].add_validator({
87
- name: if field_names.empty?
88
- nil
89
- else
90
- ((field_names.size == 1) ? field_names.first : field_names)
91
- end,
92
- block: block
93
- })
94
- end
95
- end
96
-
97
- # Add class-level schema accessor method (e.g. MyClass.inputs)
98
- Utils.define_with_collision_policy(singleton_class, schema_name, collision: collision) do
99
- define_singleton_method(schema_name) do
100
- @_expectant_schemas[schema_name]
101
- end
102
- end
103
-
104
- # Add instance-level schema accessor method (e.g. instance.inputs)
105
- if !instance_methods(false).include?(schema_name)
106
- define_method(schema_name) do
107
- BoundSchema.new(self, self.class.instance_variable_get(:@_expectant_schemas)[schema_name])
108
- end
109
- elsif collision == :force
110
- define_method(schema_name) do
111
- BoundSchema.new(self, self.class.instance_variable_get(:@_expectant_schemas)[schema_name])
112
- end
113
- elsif collision == :error
114
- raise ConfigurationError, "Instance method #{schema_name} already defined"
115
- end
116
- end
117
- end
118
- end
119
- end
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "utils"
4
+ require_relative "schema"
5
+ require_relative "bound_schema"
6
+
7
+ module Expectant
8
+ module DSL
9
+ def self.included(base)
10
+ base.include(Types)
11
+ base.extend(ClassMethods)
12
+ end
13
+
14
+ module ClassMethods
15
+ def self.extended(base)
16
+ base.class_eval do
17
+ @_expectant_schemas = {}
18
+ end
19
+ end
20
+
21
+ def inherited(sub)
22
+ return unless instance_variable_defined?(:@_expectant_schemas)
23
+ return if @_expectant_schemas.empty?
24
+
25
+ # Deep copy each schema (fields + validators)
26
+ parent_schemas = @_expectant_schemas
27
+ duped = parent_schemas.transform_values { |schema| schema.duplicate }
28
+
29
+ sub.instance_variable_set(:@_expectant_schemas, duped)
30
+ end
31
+
32
+ # Define a new expectation schema
33
+ # Options:
34
+ # collision: :error|:force -> method name collision policy for dynamic definitions
35
+ # singular: string|symbol -> control singular name for the schema
36
+ # validation: :permissive|:strict -> unknown key handling
37
+ def expects(schema_name, collision: :error, singular: nil, validation: :permissive)
38
+ field_method_name = Utils.singularize(schema_name.to_sym)
39
+ if !singular.nil?
40
+ if singular.is_a?(String) || singular.is_a?(Symbol)
41
+ field_method_name = singular.to_sym
42
+ else
43
+ raise ConfigurationError, "Invalid singular option: #{singular.inspect}"
44
+ end
45
+ end
46
+
47
+ validation = validation.to_sym
48
+ if ![:permissive, :strict].include?(validation)
49
+ raise ConfigurationError, "Invalid validation option: #{validation.inspect}"
50
+ end
51
+
52
+ schema = schema_name.to_sym
53
+
54
+ if @_expectant_schemas.key?(schema)
55
+ raise SchemaError, "Schema :#{schema} already defined"
56
+ else
57
+ create_schema(
58
+ schema,
59
+ collision: collision,
60
+ field_method_name: field_method_name,
61
+ validation: validation
62
+ )
63
+ end
64
+
65
+ self
66
+ end
67
+
68
+ def reset_inherited_expectations!
69
+ @_expectant_schemas = {}
70
+ end
71
+
72
+ private
73
+
74
+ def create_schema(schema_name, collision: :error, field_method_name: nil, validation: :permissive)
75
+ @_expectant_schemas[schema_name] = Schema.new(schema_name, validation: validation)
76
+
77
+ # Dynamically define the field definition method
78
+ # (e.g. input for :inputs, datum for :data)
79
+ Utils.define_with_collision_policy(singleton_class, field_method_name, collision: collision) do
80
+ define_singleton_method(field_method_name) do |name, **options|
81
+ expectation = Expectation.new(name, **options)
82
+ @_expectant_schemas[schema_name].add_field(expectation)
83
+ expectation
84
+ end
85
+ end
86
+
87
+ # Reset a schema
88
+ reset_method_name = "reset_#{schema_name}!"
89
+ Utils.define_with_collision_policy(singleton_class, reset_method_name, collision: collision) do
90
+ define_singleton_method(reset_method_name) do
91
+ @_expectant_schemas[schema_name].reset!
92
+ end
93
+ end
94
+
95
+ # Define validators
96
+ method_name = Utils.validator_method_name(field_method_name, Expectant.configuration)
97
+ Utils.define_with_collision_policy(singleton_class, method_name, collision: collision) do
98
+ define_singleton_method(method_name) do |*field_names, &block|
99
+ @_expectant_schemas[schema_name].add_validator({
100
+ name: if field_names.empty?
101
+ nil
102
+ else
103
+ ((field_names.size == 1) ? field_names.first : field_names)
104
+ end,
105
+ block: block
106
+ })
107
+ end
108
+ end
109
+
110
+ # Add class-level schema accessor method (e.g. MyClass.inputs)
111
+ Utils.define_with_collision_policy(singleton_class, schema_name, collision: collision) do
112
+ define_singleton_method(schema_name) do
113
+ @_expectant_schemas[schema_name]
114
+ end
115
+ end
116
+
117
+ # Add instance-level schema accessor method (e.g. instance.inputs)
118
+ if !instance_methods(false).include?(schema_name)
119
+ define_method(schema_name) do
120
+ BoundSchema.new(self, self.class.instance_variable_get(:@_expectant_schemas)[schema_name])
121
+ end
122
+ elsif collision == :force
123
+ define_method(schema_name) do
124
+ BoundSchema.new(self, self.class.instance_variable_get(:@_expectant_schemas)[schema_name])
125
+ end
126
+ elsif collision == :error
127
+ raise ConfigurationError, "Instance method #{schema_name} already defined"
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -1,8 +1,8 @@
1
- # frozen_string_literal: true
2
-
3
- module Expectant
4
- class Error < StandardError; end
5
- class ConfigurationError < Error; end
6
- class SchemaError < Error; end
7
- class ValidationError < Error; end
8
- end
1
+ # frozen_string_literal: true
2
+
3
+ module Expectant
4
+ class Error < StandardError; end
5
+ class ConfigurationError < Error; end
6
+ class SchemaError < Error; end
7
+ class ValidationError < Error; end
8
+ end
@@ -1,37 +1,37 @@
1
- # frozen_string_literal: true
2
-
3
- module Expectant
4
- class Expectation
5
- attr_reader :name, :type, :dry_type, :fallback, :default
6
-
7
- def initialize(name, type: nil, default: nil, optional: false, fallback: nil, **options)
8
- @name = name
9
- @type = type
10
- @default = default
11
- @fallback = fallback
12
- @options = options
13
-
14
- base_type = Types.resolve(type)
15
- base_type = base_type.optional if optional
16
- # if default is a proc, we call it at runtime
17
- @dry_type = if !default.nil? && !default.respond_to?(:call)
18
- base_type.default(default)
19
- else
20
- base_type
21
- end
22
- end
23
-
24
- # A field is required if it's not optional and has no default
25
- def required?
26
- !@dry_type.optional? && !has_default?
27
- end
28
-
29
- def has_default?
30
- @dry_type.default? || !@default.nil?
31
- end
32
-
33
- def has_fallback?
34
- !@fallback.nil?
35
- end
36
- end
37
- end
1
+ # frozen_string_literal: true
2
+
3
+ module Expectant
4
+ class Expectation
5
+ attr_reader :name, :type, :dry_type, :fallback, :default, :metadata
6
+
7
+ def initialize(name, type: nil, default: nil, optional: false, fallback: nil, **options)
8
+ @name = name
9
+ @type = type
10
+ @default = default
11
+ @fallback = fallback
12
+ @metadata = options
13
+
14
+ base_type = Types.resolve(type)
15
+ base_type = base_type.optional if optional
16
+ # if default is a proc, we call it at runtime
17
+ @dry_type = if !default.nil? && !default.respond_to?(:call)
18
+ base_type.default(default)
19
+ else
20
+ base_type
21
+ end
22
+ end
23
+
24
+ # A field is required if it's not optional and has no default
25
+ def required?
26
+ !@dry_type.optional? && !has_default?
27
+ end
28
+
29
+ def has_default?
30
+ @dry_type.default? || !@default.nil?
31
+ end
32
+
33
+ def has_fallback?
34
+ !@fallback.nil?
35
+ end
36
+ end
37
+ end
@@ -1,94 +1,98 @@
1
- # frozen_string_literal: true
2
-
3
- require "dry/validation"
4
-
5
- module Expectant
6
- class Schema
7
- attr_reader :name, :fields, :validators
8
-
9
- def initialize(name)
10
- @name = name
11
- @fields = []
12
- @validators = []
13
- @contract_class = nil
14
- end
15
-
16
- def keys
17
- @fields.map(&:name)
18
- end
19
-
20
- def contract
21
- @contract_class ||= build_contract
22
- end
23
-
24
- def duplicate
25
- dup = self.class.new(@name)
26
- dup.instance_variable_set(:@fields, @fields.dup)
27
- dup.instance_variable_set(:@validators, @validators.dup)
28
- dup
29
- end
30
-
31
- def add_field(expectation)
32
- @fields << expectation
33
- @contract_class = nil
34
- end
35
-
36
- def add_validator(validator_def)
37
- @validators << validator_def
38
- @contract_class = nil
39
- end
40
-
41
- def freeze
42
- @fields.freeze
43
- @validators.freeze
44
- super
45
- end
46
-
47
- def reset!
48
- @fields = []
49
- @validators = []
50
- @contract_class = nil
51
- end
52
-
53
- private
54
-
55
- def build_contract
56
- fields = @fields
57
- validators = @validators
58
-
59
- Class.new(Dry::Validation::Contract) do
60
- # Enable option passing (allows context and instance to be passed at validation time)
61
- option :context, default: proc { {} }
62
- option :instance, optional: true
63
-
64
- # Define schema based on fields using dry-types
65
- params do
66
- fields.each do |field|
67
- dry_type = field.dry_type
68
-
69
- if field.required?
70
- required(field.name).value(dry_type)
71
- else
72
- optional(field.name).value(dry_type)
73
- end
74
- end
75
- end
76
-
77
- # Add custom validators
78
- validators.each do |validator_def|
79
- if validator_def[:name]
80
- # Single key or multiple keys
81
- if validator_def[:name].is_a?(Array)
82
- rule(*validator_def[:name], &validator_def[:block])
83
- else
84
- rule(validator_def[:name], &validator_def[:block])
85
- end
86
- else
87
- # Global rule without a specific field
88
- rule(&validator_def[:block])
89
- end
90
- end
91
- end
92
- end
93
- end
94
- end
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/validation"
4
+
5
+ module Expectant
6
+ class Schema
7
+ attr_reader :name, :fields, :validators, :validation
8
+
9
+ def initialize(name, validation: :permissive)
10
+ @name = name
11
+ @fields = []
12
+ @validators = []
13
+ @contract_class = nil
14
+ @validation = validation
15
+ end
16
+
17
+ def keys
18
+ @fields.map(&:name)
19
+ end
20
+
21
+ def contract
22
+ @contract_class ||= build_contract
23
+ end
24
+
25
+ def duplicate
26
+ dup = self.class.new(@name, validation: @validation)
27
+ dup.instance_variable_set(:@fields, @fields.dup)
28
+ dup.instance_variable_set(:@validators, @validators.dup)
29
+ dup
30
+ end
31
+
32
+ def add_field(expectation)
33
+ @fields << expectation
34
+ @contract_class = nil
35
+ end
36
+
37
+ def add_validator(validator_def)
38
+ @validators << validator_def
39
+ @contract_class = nil
40
+ end
41
+
42
+ def freeze
43
+ @fields.freeze
44
+ @validators.freeze
45
+ super
46
+ end
47
+
48
+ def reset!
49
+ @fields = []
50
+ @validators = []
51
+ @contract_class = nil
52
+ end
53
+
54
+ private
55
+
56
+ def build_contract
57
+ fields = @fields
58
+ validators = @validators
59
+ validation_mode = @validation
60
+
61
+ Class.new(Dry::Validation::Contract) do
62
+ # Enable option passing (allows context and instance to be passed at validation time)
63
+ option :context, default: proc { {} }
64
+ option :instance, optional: true
65
+
66
+ # Define schema based on fields using dry-types
67
+ params do
68
+ config.validate_keys = true if validation_mode == :strict
69
+
70
+ fields.each do |field|
71
+ dry_type = field.dry_type
72
+
73
+ if field.required?
74
+ required(field.name).value(dry_type)
75
+ else
76
+ optional(field.name).value(dry_type)
77
+ end
78
+ end
79
+ end
80
+
81
+ # Add custom validators
82
+ validators.each do |validator_def|
83
+ if validator_def[:name]
84
+ # Single key or multiple keys
85
+ if validator_def[:name].is_a?(Array)
86
+ rule(*validator_def[:name], &validator_def[:block])
87
+ else
88
+ rule(validator_def[:name], &validator_def[:block])
89
+ end
90
+ else
91
+ # Global rule without a specific field
92
+ rule(&validator_def[:block])
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -1,56 +1,56 @@
1
- # frozen_string_literal: true
2
-
3
- require "dry-types"
4
-
5
- module Expectant
6
- module Types
7
- include Dry.Types()
8
-
9
- def self.resolve(type_definition)
10
- case type_definition
11
- when nil
12
- Any
13
- when Symbol
14
- resolve_symbol(type_definition)
15
- when Dry::Types::Type
16
- # Already a dry-type, return as-is
17
- type_definition
18
- when Class
19
- Instance(type_definition)
20
- else
21
- raise ConfigurationError, "Invalid type definition: #{type_definition}"
22
- end
23
- end
24
-
25
- def self.resolve_symbol(symbol)
26
- case symbol
27
- when :string, :str
28
- Params::String
29
- when :integer, :int
30
- Params::Integer
31
- when :float
32
- Params::Float
33
- when :decimal
34
- Params::Decimal
35
- when :boolean, :bool
36
- Params::Bool
37
- when :date
38
- Params::Date
39
- when :datetime
40
- Params::DateTime
41
- when :time
42
- Params::Time
43
- when :array
44
- Params::Array
45
- when :hash
46
- Params::Hash
47
- when :symbol, :sym
48
- Symbol
49
- when :any, :nil
50
- Any
51
- else
52
- raise ConfigurationError, "Unknown type symbol: #{symbol}"
53
- end
54
- end
55
- end
56
- end
1
+ # frozen_string_literal: true
2
+
3
+ require "dry-types"
4
+
5
+ module Expectant
6
+ module Types
7
+ include Dry.Types()
8
+
9
+ def self.resolve(type_definition)
10
+ case type_definition
11
+ when nil
12
+ Any
13
+ when Symbol
14
+ resolve_symbol(type_definition)
15
+ when Dry::Types::Type
16
+ # Already a dry-type, return as-is
17
+ type_definition
18
+ when Class
19
+ Instance(type_definition)
20
+ else
21
+ raise ConfigurationError, "Invalid type definition: #{type_definition}"
22
+ end
23
+ end
24
+
25
+ def self.resolve_symbol(symbol)
26
+ case symbol
27
+ when :string, :str
28
+ Params::String
29
+ when :integer, :int
30
+ Params::Integer
31
+ when :float
32
+ Params::Float
33
+ when :decimal
34
+ Params::Decimal
35
+ when :boolean, :bool
36
+ Params::Bool
37
+ when :date
38
+ Params::Date
39
+ when :datetime
40
+ Params::DateTime
41
+ when :time
42
+ Params::Time
43
+ when :array
44
+ Params::Array
45
+ when :hash
46
+ Params::Hash
47
+ when :symbol, :sym
48
+ Symbol
49
+ when :any, :nil
50
+ Any
51
+ else
52
+ raise ConfigurationError, "Unknown type symbol: #{symbol}"
53
+ end
54
+ end
55
+ end
56
+ end