dry-rb / dry-schema Goto Github PK
View Code? Open in Web Editor NEWCoercion and validation for data structures
Home Page: https://dry-rb.org/gems/dry-schema
License: MIT License
Coercion and validation for data structures
Home Page: https://dry-rb.org/gems/dry-schema
License: MIT License
Result#errors
and sibling methods are returning a list when there are errors on the predicate given as key or nil
when there is none.
When there is no items for a list it makes more sense to return the empty list instead of nil
, this way you don't have to ask for the type in your code:
# Now
result.errors.map(&:html_for_errors) unless result.errors.nil?
# With the proposal
result.errors.map(&:html_for_errors)
I would happily try a PR if you like the idea.
I'd like to have the ability to change a key for the success case, to that:
params = {address: {city: "London"}}
result = schema.call(params)
result.output => {"address_attributes" => {"city" => "London" }}
As you might have guessed, I'm trying to make active record nested assignment happen :)
It would be pretty cool if we could define how errors should be build. I'm talking about something like that: https://stackoverflow.com/a/19673344/3837806
It would be nice if Dry::Validation.JSON
and Dry::Validation.Form
had their own error message namespace. Something like this:
en:
schema:
errors: &default
array?: "must be an array"
empty?: "must be empty"
# ...
hash?: "must be a hash"
# ...
json:
errors:
<<: *default
hash?: "must be an object"
This way, when you build a validation using one of this domain specific schemas you would get domain specific messages:
DEFINITION = proc { required('thing').value(:hash?, :empty?) }
json_schema = Dry::Validation.JSON(&DEFINITION)
normal_schema = Dry::Validation.Schema(&DEFINITION)
input = { 'thing' => [] }
normal_schema.call(input).messages # => {"thing"=>["must be a hash", "must be empty"]}
json_schema.call(input).messages # => {"thing"=>["must be an object", "must be empty"]}
Ex:
{:should_have_investment_rate_only=>["must be filled", "must be filled", "must be filled", "cannot be defined"], 2=>{:minimum_investment=>["is missing"]}}
The rule :should_have_investment_rate_only
return an array of error messages, but it's not possible to know if it was on item 1,2 or 42. It would be better if it worked like the other rules.
The schema was something like:
BATCH_SCHEMA = Dry::Validation.JSON do
each do
schema(OTHER_SCHEMA)
end
end
OTHER_SCHEMA = Dry::Validation.JSON do
required(:minimum_investment) #blah blah
rule(:should_have_investment_rate_only) do
# the rule
end
end
I know this was somewhat discussed before, and there are probably not many people who are bothered by this, but I figured I would post here anyway.
The fact that you are adding i18n keys to the already existing errors
namespace causes problems for me. As far as I could see in other gems - for example devise - it is common for a gem to put their i18n keys under their own root namespace.
Also - to give a little context - I should mention that I am not using dry-validations
at all, and the gem was installed indirectly for me by the config
gem - so from my point of view, its a "feature" I am not using, and is causing some issues in my codebase, and I have no way of removing, other than to stop using the config
gem.
I guess my suggestion is - if at all possible - is to namespace your i18n keys under some dry_validations:
root node.
This can be reproduced when used with config.messages = :i18n
, may also apply to custom messages.
begin
require 'bundler/inline'
rescue LoadError => e
$stderr.puts 'Bundler version 1.10 or later is required'
raise e
end
gemfile(true) do
source 'https://rubygems.org'
gem 'dry-validation', '0.11.1', require: false
gem 'i18n', '0.8.6', require: false
gem 'rspec', require: 'rspec/autorun'
end
require 'i18n'
require 'dry-validation'
UserForm = Dry::Validation.Schema do
configure do
config.messages = :i18n
config.namespace = :user
end
required(:name).filled(:str?)
end
I18n.available_locales = %i[en ru]
I18n.locale = I18n.default_locale = :en
I18n.backend.store_translations(
:en,
errors: {
rules: {
user: {
rules: {
name: {
filled?: 'must be filled'
}
}
}
}
}
)
I18n.backend.store_translations(
:ru,
errors: {
rules: {
user: {
rules: {
name: {
filled?: 'заполните это поле'
}
}
}
}
}
)
RSpec.describe 'I18n validation messages' do
subject(:result) { UserForm.call(name: nil) }
around do |example|
# Set Russian locale for all examples
I18n.with_locale(:ru) { example.run }
end
EXPECTED = ['заполните это поле'].freeze
context 'with new implicit options ({ hints: false })' do
it { expect(result.errors[:name]).to eq(EXPECTED) }
end
context 'with explicit locale' do
it { expect(result.messages(locale: I18n.locale)[:name]).to eq(EXPECTED) }
end
# Failing example!
context 'when reusing default MessageCompiler instance' do
it { expect(result.messages[:name]).to eq(EXPECTED) }
end
end
# Monkey patch MessageCompiler to make the failing example pass:
#
# module PatchMessageCompiler
# def default_lookup_options
# { locale: messages.default_locale }
# end
# end
#
# Dry::Validation::MessageCompiler.prepend PatchMessageCompiler
All tests pass.
One test fails.
After some debugging I've found out that messages are fetched in MessageCompiler#visit_predicate
:
msg_opts
is a hash with options passed to I18n.t
. It contains the :locale
option deduced from default_lookup_options
, which is set in the constructor using messages.default_locale
(active locale at the moment):
MessageCompiler
is instantiated when Schema
subclass is created. It can be instantiated one more time when validation error messages are retrieved via MessageCompiler#with
, but only when provided with non-default (non-empty) options, otherwise the first created instance of MessageCompiler
is reused:
So we have the following problem:
MessageCompiler
is instantiated when the schema subclass is created with the locale which was active at that moment;Result#messages
method (with empty options hash);MessageCompiler
is reused, so we get messages in the locale which was active when that instance was created, not in the current locale.If some options are passed to Result#messages
explicitly or implicitly (Result#errors
passes { hints: false }
, Result#hints
passes { failures: false }
), then MessageCompiler#with
returns new instance that has the current locale as the :locale
options in default_lookup_options
, so we get messages in the current locale.
Make #default_lookup_options
fetch locale each time it's invoked (see how I monkey-patched MessageCompiler
in the example above).
Change MessageCompiler#with
so it takes the current value of messages.default_locale
into account when deciding whether to create a new instance:
def with(new_options)
return self if new_options.empty? && locale == messages.default_locale
self.class.new(messages, options.merge(new_options))
end
Don't pass :locale
option to Messages::Abstract#call
?
Hi guys, I've encountered a strange behavior in custom error messages for duplicate keys within different namespaces. Example:
I'm having 2 schemas:
# test.rb
student = Dry::Validation.Schema do
configure do
config.messages_file = './errors.yml'
config.namespace = :student
end
required(:name).filled
end
teacher = Dry::Validation.Schema do
configure do
config.messages_file = './errors.yml'
config.namespace = :teacher
end
required(:name).filled
end
And an errors.yml
file with custom error messages:
en:
errors:
rules:
student:
rules:
name:
filled?: Student - name is missing
teacher:
rules:
name:
filled?: Teacher - name is missing
It seems as if the order in which schemas are called has an effect on the output of error messages. See: run the test.rb
script (with a breakpoint after schemas definitions):
[1] pry(main)> teacher.call(name: "").messages
=> {:name=>["Teacher - name is missing"]}
[2] pry(main)> student.call(name: "").messages
=> {:name=>["Teacher - name is missing"]}
Run the script again and call schemas in different order:
[1] pry(main)> student.call(name: "").messages
=> {:name=>["Student - name is missing"]}
[2] pry(main)> teacher.call(name: "").messages
=> {:name=>["Student - name is missing"]}
After calling the first schema an error message for subsequent schemas with duplicate key is repeated (as if it would be cached somewhere) - namespace is ignored. I'm not sure whether it is a bug or I'm just defining my schemas/error messages file in incorrect way. Has anyone encountered the same problem?
Maybe this is a dumb question but I really didn't find it anywhere in the documentation.
How can I translate a Schema field name? Example:
UserValidation = Dry::Validation.Schema do
configure { config.messages = :i18n }
required(:age).filled
end
When I want the full messages i got the message translated correctly, but not the field name.
age obrigatório(a)
. I didn't find the translation key for 'age', which should be 'idade' of the translated name.
Maybe the yml file should have something like this?
pt_BR:
errors:
fields:
age: Idade
Sorry about my english. To be clearer I want to know the translation key to be able to translate the Schema fields. If I have required(:name).filled
I want to setup my i18n yml file to have 'name' translated in the full messages validations.
Thanks for everything and congratulations for the excelent lib! 👍
Dry::Schema::PredicateInferrer[Dry::Types["strict.string"].enum("foo", "bar")]
Traceback (most recent call last):
10: from ./bin/console:16:in `<main>'
9: from (irb):1:in `<main>'
8: from /Users/tim/Source/github.com/dry-rb/dry-schema/lib/dry/schema/predicate_inferrer.rb:79:in `[]'
7: from /Users/tim/.rbenv/versions/2.6.0/lib/ruby/gems/2.6.0/gems/dry-core-0.4.7/lib/dry/core/cache.rb:49:in `fetch_or_store'
6: from /Users/tim/.rbenv/versions/2.6.0/lib/ruby/gems/2.6.0/gems/concurrent-ruby-1.1.4/lib/concurrent/map.rb:192:in `fetch_or_store'
5: from /Users/tim/.rbenv/versions/2.6.0/lib/ruby/gems/2.6.0/gems/concurrent-ruby-1.1.4/lib/concurrent/map.rb:172:in `fetch'
4: from /Users/tim/.rbenv/versions/2.6.0/lib/ruby/gems/2.6.0/gems/concurrent-ruby-1.1.4/lib/concurrent/map.rb:193:in `block in fetch_or_store'
3: from /Users/tim/Source/github.com/dry-rb/dry-schema/lib/dry/schema/predicate_inferrer.rb:80:in `block in []'
2: from /Users/tim/Source/github.com/dry-rb/dry-schema/lib/dry/schema/predicate_inferrer.rb:30:in `visit'
1: from /Users/tim/Source/github.com/dry-rb/dry-schema/lib/dry/schema/predicate_inferrer.rb:30:in `public_send'
NoMethodError (undefined method `visit_enum' for #<Dry::Schema::PredicateInferrer::Compiler:0x00007fa821348c50>)
Did you mean? visit_sum
Given there's a search form, where all fields are optional, and a validation schema that receives the form raw data via rack.
As we know, the form will be submitted with empty strings as values for empty fields.
Extremely simplified, the params could look something like:
Params = Dry::Validation.Form do
optional(:date).maybe(:date?) # or required, as it's a form, it will be set anyway
end
So, when the form submits "" for the date param, the validation output will look like { date: nil }
, and we have to handle this nil value somewhere.
Is there any way to explicitly say that this param can be ignored if nil? Like "ok, I know it can be empty or nil, but if it is, please just don't put it in the output", so that the output would look like {}
and we'd not have to handle the nil value elsewhere.
require 'dry-validation'
Data = Dry::Validation.Schema do
required(:age).filled(:number?)
end
ChangedData = Dry::Validation.Schema(Data) do
required(:age).filled(:number?, gteq?: 18)
end
Form = Dry::Validation.Schema do
required(:user).schema(ChangedData)
end
BadForm = Dry::Validation.Schema do
required(:user).schema(Data) do
required(:age).filled(:number?, gteq?: 18)
end
end
p Form.call(user: { age: 15 })
#<Dry::Validation::Result output={:user=>{:age=>15}} errors={:user=>{:age=>["must be greater than or equal to 18"]}}>
p BadForm.call(user: { age: 15 })
#<Dry::Validation::Result output={:user=>{:age=>15}} errors={}>
If I set namespace for my schema and use full messages option, this will not get translated:
Schema = Dry::Validation.Params do
configure do
config.namespace = :my_namespace
end
required(:my_rule).filled
end
en:
rules:
my_namespace:
my_rule: Rule translated
Schema.(my_rule: '').messages(full: true) # should output 'Rule translated must be filled'
On the other hand, if i didn't set namespace it will work as intended:
en:
rules:
my_rule: Rule translated
I'm not sure if it's a bug or but looks like one.
see spec/schema/inheriting_schema_spec.rb ....
Situation : I have a number of fields that are shared throughout my application which are not nested schema - but i would like to define a single-field validation schema for each and include as needed in other schema ... the schema-inheritance works for nested (compound) hashes - see above. can we also support for non-nested... here's the above spec with 'flat' field added which fails ...
require 'dry-validation'
RSpec.describe 'Reusing schemas' do
subject(:schema) do
Dry::Validation.Schema do
required(:city).filled
required(:location).schema(LocationSchema)
required(:country).schema(CountrySchema)
end
end
before do
LocationSchema = Dry::Validation.Schema do
configure { config.input_processor = :form }
required(:lat).filled(:float?)
required(:lng).filled(:float?)
end
# define a 'flat' (scalar) schema
CountrySchema = Dry::Validation.Schema do
configure { config.input_processor = :form }
required(:country).filled(:str?)
end
end
after do
Object.send(:remove_const, :LocationSchema)
end
it 're-uses existing schema' do
expect(schema.(city: 'NYC', country: 'usa', location: { lat: 1.23, lng: 45.6 })).to be_success
expect(schema.(city: 'NYC', country: 'usa', location: { lat: nil, lng: '45.6' }).messages).to eql(
location: {
lat: ['must be filled'],
lng: ['must be a float']
}
)
end
end
Thanks !
I encounter an issue when trying out the library to validate Rails controller params.
module ParamSchemas
UserSchema = Dry::Validation.Form do
required(:user).schema do
required(:name).filled(:str?)
required(:age).filled(:int?, gt?: 18)
end
end
end
http_params = {}
params = ActionController::Parameters.new(http_params)
result = ParamSchemas::UserSchema.(params.to_unsafe_h)
result.hints
=> TypeError: no implicit conversion of Symbol into Integer
from /Users/viett/.rvm/gems/ruby-2.4.1/gems/dry-validation-0.11.1/lib/dry/validation/message_set.rb:85:in `[]'
This only happens when http_params
is an empty hash.
When http_params = { user: '' }
, things work fine.
Enum validation from dry-types doesn't seem to work with type_specs
require 'dry-types'
require 'dry-validation'
State = Dry::Types['strict.string'].enum('enabled', 'disabled')
schema = Dry::Validation.Form do
configure { config.type_specs = true }
required(:state, State).filled
end
schema.(state: "enabled").errors
=> {}
schema.(state: "foo").errors
=> {:state=>["must be one of: enabled, disabled"]}
Please note that It works as described in: dry-rb/dry-validation#348
for example required(:state).filled(State)
makes the above example work properly
I'm trying to use this lib for schema validation, and there is this use case where a key needs to be filled with a default, when nothing is passed.
I know that this wasn't in the scope of dry-validation, but that this is dry-schema, maybe the use case now fits.
Something like:
optional(:op_type).default("read")
Hi,
This problem is because ruby throws "too many arguments for format string" when $DEBUG
is true and placeholders and arguments in string templates are not matched.
I was on dry-validation-0.10.5
Below, I put together a code snippet that would reproduce the behaviour. In this particular case demonstrated by my code snippet, it failed at the beginning of message_complier.rb#message_text
when the template was 'is missing'
and the tokens were { input: null }
.
I ran into this when running hanami in debug, and this would halt it.
I just wonder if this is an improvement opportunity.
Many thanks,
mytram
require 'dry-validation'
$DEBUG = true
schema = Dry::Validation.Schema do
required(:name).filled
end
result = schema.call(
email: '[email protected]',
address: { street: 'Street 1', city: 'NYC', zipcode: '1234' }
)
# this will throw ArguementError as $DEBUG is on
puts "error message: #{result.messages.inspect}"
# .rbenv/versions/2.4.0/lib/ruby/gems/2.4.0/gems/dry-validation-0.10.5/lib/dry/validation/message_compiler.rb:163:in `%': too many arguments for format string (ArgumentError)
A few cases I found during re-writing my dry-v schemas to dry-schema
Safe type
Dry::Schema::PredicateInferrer[Dry::Types['params.bool']]
# NoMethodError (undefined method `primitive' for #<Dry::Types::Safe:0x00007fdd1f059ea8>)
Sum type with constructor
non_empty_string = Dry::Types['string'].optional.constructor { |str| str.empty? ? nil : str }
Dry::Schema::PredicateInferrer[non_empty_string]
# NoMethodError (undefined method `primitive' for #<Dry::Types::Sum:0x00007fdd1f0ec3c0>)
The JSON standard defines the following data types
null
(= nil
)If any JSON schema receives any value of the types above it shouldn't raise an exception at least. Better yet it should provide a helpful error message.
I have a schema with an optional key w/o bounds, dry-schema complains when it's not passed
Dry::Schema.define { optional(:value) }.({})
# => #<Dry::Schema::Result{} errors={:value=>["is missing"]}>
whereas adding any specification changes this behavior
Dry::Schema.define { optional(:value).filled }.({})
# => #<Dry::Schema::Result{} errors={}>
I would expect optional(:value)
to be enough in this case.
The same could be said of ruby 2.2 .
For context: I'm in the process of porting an outdated dry-validation integration to dry-schema. I tried first with 0.1.1, but ran into the bug related with the error messages not being consistent with nested schemas. I've seen the changelog mentions for 0.2.0, and wanted to give it a try, but this project runs ruby 2.3, so no can do.
Now, migrating to ruby 2.4 is way less trivial than migrating our schema validators (it's a fairly big app). And I don't know exactly what features depend on ruby 2.4 tbh. Are there any? Because unless these fixes are backported to 0.1.x, I'll be stuck with unsupported dry-validation
until we migrate to 2.4, which might take some time. :/
When ruby runs with warnings enabled, the strangest thing happens, following warnings show up:
/Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema.rb:17: warning: in `define': the last argument was passed as a single Hash
/Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema.rb:27: warning: although a splat keyword arguments here
then, when trying to apply a schema and errors are being generated, it crashes with SystemStackError
:
SystemStackError:
stack level too deep
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:179:in `each_with_object'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:179:in `message_tokens'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:106:in `visit_predicate'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:50:in `visit'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:79:in `block in visit_and'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:79:in `map'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:79:in `visit_and'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:50:in `visit'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:45:in `block in call'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:45:in `map'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:45:in `call'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/result.rb:142:in `message_set'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/result.rb:106:in `errors'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-equalizer-0.2.1/lib/dry/equalizer.rb:76:in `map'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-equalizer-0.2.1/lib/dry/equalizer.rb:76:in `block in define_hash_method'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/messages/abstract.rb:78:in `hash'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/messages/abstract.rb:78:in `hash'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/messages/abstract.rb:78:in `call'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:116:in `visit_predicate'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:50:in `visit'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:79:in `block in visit_and'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:79:in `map'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:79:in `visit_and'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:50:in `visit'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:45:in `block in call'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:45:in `map'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:45:in `call'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/result.rb:142:in `message_set'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/result.rb:106:in `errors'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/result.rb:151:in `inspect'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/messages/abstract.rb:99:in `inspect'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/messages/abstract.rb:99:in `reject'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/messages/abstract.rb:99:in `lookup'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/messages/abstract.rb:79:in `block in call'
# /Users/solnic/.gem/ruby/2.6.1/gems/concurrent-ruby-1.1.4/lib/concurrent/map.rb:193:in `block in fetch_or_store'
# /Users/solnic/.gem/ruby/2.6.1/gems/concurrent-ruby-1.1.4/lib/concurrent/map.rb:172:in `fetch'
# /Users/solnic/.gem/ruby/2.6.1/gems/concurrent-ruby-1.1.4/lib/concurrent/map.rb:192:in `fetch_or_store'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/messages/abstract.rb:78:in `call'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:116:in `visit_predicate'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:50:in `visit'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:79:in `block in visit_and'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:79:in `map'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:79:in `visit_and'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:50:in `visit'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:45:in `block in call'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:45:in `map'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:45:in `call'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/result.rb:142:in `message_set'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/result.rb:106:in `errors'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/result.rb:151:in `inspect'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/messages/abstract.rb:99:in `inspect'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/messages/abstract.rb:99:in `reject'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/messages/abstract.rb:99:in `lookup'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/messages/abstract.rb:79:in `block in call'
# /Users/solnic/.gem/ruby/2.6.1/gems/concurrent-ruby-1.1.4/lib/concurrent/map.rb:193:in `block in fetch_or_store'
# /Users/solnic/.gem/ruby/2.6.1/gems/concurrent-ruby-1.1.4/lib/concurrent/map.rb:172:in `fetch'
# /Users/solnic/.gem/ruby/2.6.1/gems/concurrent-ruby-1.1.4/lib/concurrent/map.rb:192:in `fetch_or_store'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/messages/abstract.rb:78:in `call'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:116:in `visit_predicate'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:50:in `visit'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:79:in `block in visit_and'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:79:in `map'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:79:in `visit_and'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:50:in `visit'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:45:in `block in call'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:45:in `map'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:45:in `call'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/result.rb:142:in `message_set'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/result.rb:106:in `errors'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/result.rb:151:in `inspect'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/messages/abstract.rb:99:in `inspect'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/messages/abstract.rb:99:in `reject'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/messages/abstract.rb:99:in `lookup'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/messages/abstract.rb:79:in `block in call'
# /Users/solnic/.gem/ruby/2.6.1/gems/concurrent-ruby-1.1.4/lib/concurrent/map.rb:193:in `block in fetch_or_store'
# /Users/solnic/.gem/ruby/2.6.1/gems/concurrent-ruby-1.1.4/lib/concurrent/map.rb:172:in `fetch'
# /Users/solnic/.gem/ruby/2.6.1/gems/concurrent-ruby-1.1.4/lib/concurrent/map.rb:192:in `fetch_or_store'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/messages/abstract.rb:78:in `call'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:116:in `visit_predicate'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:50:in `visit'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:79:in `block in visit_and'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:79:in `map'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:79:in `visit_and'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:50:in `visit'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:45:in `block in call'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:45:in `map'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/message_compiler.rb:45:in `call'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/result.rb:142:in `message_set'
# /Users/solnic/.gem/ruby/2.6.1/gems/dry-schema-0.1.0/lib/dry/schema/result.rb:106:in `errors'
en:
errors:
rules:
email:
filled?: "the email is missing"
user:
filled?: "name cannot be blank"
rules:
address:
filled?: "You gotta tell us where you live"
Please example for more nested rule message.
Dry::Schema.Params { required(:foo).value(:array).each(Types::Params::Integer) }
rule_compiler.rb:23:in `visit': undefined method `visit_constructor' for #<Dry::Schema::Compiler:0x00007fcd389d4b50>
throws an error as does
Dry::Schema.Params { required(:foo).value(:array).each { value(Types::Strict::Integer) } }
We likely want to support both options
Here is a sample code that demonstrates the problem:
module T
include Dry::Types.module
Foo = T::String
end
s = Dry::Schema.build do
configure do
config.type_specs = true
config.input_processor = :sanitizer
end
required(:foo, T::Foo).filled
required(:bar).filled
end
end;
result = s.({bar: '1', foo: '1'})
Result:
=> #<Dry::Schema::Result output={:foo=>"1"} errors={:bar=>["is missing"]}>
Expected
=> #<Dry::Schema::Result output={:foo=>"1", :bar=> "1"} errors={}>
This schema
Schema = Dry::Schema.define do
optional(:contacts).array do
hash do
required(:name).filled
end
end
end
produces an invalid error message
2.6.1 :001 > Schema.(contacts: [{ }])
=> #<Dry::Schema::Result{:contacts=>[{}]} errors={:contacts=>{0=>{:contacts=>["is missing"]}}}>
# ^see contacts => contacts
At the same time changing optional(:contacts).array
to required(:contacts).array
gives the expected result:
2.6.1 :001 > Schema.(contacts: [{ }])
=> #<Dry::Schema::Result{:contacts=>[{}]} errors={:contacts=>{0=>{:name=>["is missing"]}}}>
I'm having a problem with inheriting multiple schemas at once
Here is an example of what I mean
Sortable = Dry::Validation.Params do
optional(:sort).filled
end
Findable = Dry::Validation.Params do
required(:id).filled(:int?, gt?: 0)
end
Paginatable = Dry::Validation.Params do
optional(:page).schema do
required(:number).filled(:int?, gt?: 0)
required(:size).filled(:int?, gt?: 0)
end
end
IndexSchema = Dry::Validation.Params(
::Schemas::Concerns::Paginatable,
::Schemas::Concerns::Sortable,
::Schemas::Concerns::Findable
) do
optional(:filter).schema do
optional(:email).filled(:str?)
optional(:fullname).filled(:str?)
end
end
The problem comes from here as Params
can have only one base.
Say I have the following schema:
{
content_type: {
name: "my content type",
field_schema: {
author: { field_type: "StringField", settings: { max_length: 100 } },
some_other_generic_field_name: { field_type: "StringField", settings: { } }
}
}
}
I would expect to somehow be able to validate this:
required(:content_type).schema do
required(:name).filled(:str?)
required(:field_schema).schema do
each_unknown_key_pair do
required(:field_type).filled(:str?)
required(:settings).filled(:hash?)
end
end
end
(I was trying to do this in Hanami)
But, as you are aware, there is no DSL for each_unknown_key_pair
. I've tried using each
but this expects an Array.. so maybe we could use each_pair
, or something similar, which would work for a hash where we don't care about or know the key but just want to validate the values?
@solnic requested that I open this issue. Please let me know if I can add any more information!
Form = Dry::Schema.Params do
required(:bar).type(:integer).value(:int?)
end
FormContainer = Dry::Schema.Params do
optional(:foo).value(:array).each(Form)
end
puts FormContainer.(foo: [{ bar: '123' }]).inspect
Expected:
#<Dry::Schema::Result{:foo=>[{:bar=>123}]} errors={}>
Actual:
#<Dry::Schema::Result{:foo=>[{:bar=>\"123\"}]} errors={:foo=>{0=>{:bar=>[\"must be an integer\"]}}}>
I create a gist to explain what I mean
https://gist.github.com/niedhui/87b341fd17e9bfdfbbe2aa8ca69bd0eb
Messages::I18n#get
always had a locale options, the value is
the I18n.locale
when the schema is defined. It does not consider the current I18n.locale
(eg, change the I18n.locale
after the schema defination), which Messages::I18n#key?
does.
I know I can pass the {locale: I18n.locale} option explicitly to the Result#messages, but it seems not the way how the I18n gem works
Dry::Struct requires any attribute to be defined in an input hash, even if it's optional and with default value.
While it's logical behaviour, it may be not ok for different cases - for example for public API.
There are some API with optional attributes, that may change. And necessity to update all clients code every time API adds new optional field isn't desirable, even sometimes impractical.
Thus, such API should contain code logic to fulfil missed elements in hash with nil, which may be hard, especially in nested data structures. It's a boilerplate code, often ugly.
So it'd be helpful to have an option to make dry-validation
schema return all keys in output hash with nil
value, even there wasn't such key in input hash. Schema already has every needed attribute defined, with correspondent type, so IMHO it's good place to put such functionality in.
It may be on more input_processor
(however I saw input processor is in deprecated.rb
file).
As an example
Dry::Schema.JSON do
each do
schema do
required(:name).filled(:str?)
required(:age).filled(:int?)
end
end
end
Hi,
Are there any options to describe schema with dynamic keys?
Let's say I have a hash where keys are string numbers:
{
foo: {
'1' => { name: 'bar1' },
'2' => { name: 'bar2' },
...
'100' => { name: 'bar100' }
}
}
For now I can write a schema by using some limited predefined keys:
Dry::Validation.Schema do
required(:foo).schema do
('1'..'100').each do |position|
optional(position).schema do
required(:name).filled(:str?)
end
end
end
end
But this approach is not that flexible in case if I need to accept keys which may be any number.
Is using imperative custom validation blocks the only option to write flexible validation in this case?
BarSchema = Dry::Validation.Schema do
required(:name).filled(:str?)
end
schema = Dry::Validation.Schema do
required(:foo).filled
configure do
def self.messages
# How can I get detailed errors here from BarSchema?
super.merge(en: { errors: { valid_foo: 'foo is not valid' } })
end
end
validate(valid_foo: :foo) do |key_values|
key_values.all? do |k, v|
k ~= /\A\d+\z/ && BarSchema.call(v).success?
end
end
end
This form schema has .each
right after required
schema = Dry::Schema.Form do
required(:foo).each do
schema do
required(:bar).filled(:string)
end
end
end
and it rejects valid input
2.6.1 :001 > schema.("foo" => ["bar" => "baz"])
=> #<Dry::Schema::Result{:foo=>[{"bar"=>"baz"}]} errors={:foo=>{0=>{:bar=>["is missing"]}}}>
At the same time
schema = Dry::Schema.Form do
required(:foo).value(:array).each do
schema do
required(:bar).filled(:string)
end
end
end
works 👌
2.6.1 :001 > schema.("foo" => ["bar" => "baz"])
=> #<Dry::Schema::Result{:foo=>[{:bar=>"baz"}]} errors={}>
The fact it silently doesn't work is surprising.
The documentation link goes to http://dry-rb.org/gems/dry-schema, which is a 404.
Let's say I have two schemas:
CommentSchema = Dry::Validation.Schema(ApplicationSchema) do
configure do
config.namespace = :comment
end
required(:comment_body).filled
end
PostSchema = Dry::Validation.Schema(ApplicationSchema) do
configure do
config.namespace = :post
end
required(:post_body).filled
required(:comment).schema(CommentSchema)
end
And I have the following lines in my en.yml
:
en:
errors:
rules:
comment:
rules:
comment_body:
filled?: "COMMENT can't be blank"
post:
rules:
post_body:
filled?: "POST can't be blank"
When I call them independently error messages are correct:
PostSchema.call(post_body: '').errors
# => {:post_body=>["POST can't be blank"], :comment=>["is missing"]}
CommentSchema.call(comment_body: '').errors
# => {:comment_body=>["COMMENT can't be blank"]}
But when I put comment parameters inside post parameters dry-validation uses only default errors for nested parameters:
PostSchema.call(post_body: '', comment: { comment_body: '' })
{:post_body=>["POST can't be blank"], :comment=>{:comment_body=>["must be filled"]}}
On my project we have a very noisy i18n backend, here's the list of the keys that dry-validation checks - https://gist.github.com/iliabylich/6c0db70a547fb4ba5d14c6f3d1d5ac36. As you may see it tries to get keys like errors.rules.post.comment_body.failure
(which means that it ignores configured config.namespace
for nested schema)
First of all, is it a bug or misconfiguration? Does dry-validation support it?
What I tried to do
Schema = Dry::Schema.define do
required(:foo).value do
hash { required(:bar).value(:string) } |
array { hash { required(:baz).value(:string) } }
end
end
What it said
undefined method `array' for #<Dry::Schema::Trace:0x00007ff14b8da970>
I would love to be able to define a schema which generated errors if it was given unexpected keys:
require 'dry/validation'
schema = Dry::Validation.Schema(strict: true) do
hash? do
required(:foo).filled(:int?)
required(:bar).filled(:str?)
end
end
outcome = schema.call(foo: 1, bar: "two", baz: 3.0)
p outcome.messages # => {:baz=>["is not allowed"]}
This is really important for defining a public JSON API where you want to reject invalid user input.
The names chosen for some of the built-in predicates don't match those commonly used in other languages, increasing cognitive load and the potential for user error. Please consider using the common names instead:
dry | common |
---|---|
eql | eq |
gt | gt |
gteq | ge |
lt | lt |
lteq | le |
The dry-struct extension fails when the struct includes a Types::Hash.map attribute.
undefined method `visit_map' for #<Dry::Validation::InputProcessorCompiler::Params:0x00007f84448ab0e0> (NoMethodError)
https://github.com/dry-rb/dry-schema/blob/master/lib/dry/schema/dsl.rb#L266
it should merge current rules into parent rules, potentially overriding some parent rules.
I assume that this is also in the works, but it's the only issue barring early adoption.
Hi,
For example I have schema:
class ParamsValidator < Dry::Validation::Schema::Form
key(:a) { ... }
optional(:b) { ... }
end
How can I get list of required fields ([:a]), optional fields ([:b]) and all fields ([:a, :b])?
FYI: I want to use these lists in unit-tests for check validation of optional fields.
Taken from dry-rb/dry-validation#280
(Dry::Schema.define { required(:foo) { str? & filled? | int? } }).call({}).messages
Traceback (most recent call last):
17: from ./bin/console:40:in `<main>'
16: from /Users/gordon/dev/dry-rb/dry-schema/lib/dry/schema/result.rb:117:in `messages'
15: from /Users/gordon/dev/dry-rb/dry-schema/lib/dry/schema/result.rb:142:in `message_set'
14: from /Users/gordon/dev/dry-rb/dry-schema/lib/dry/schema/message_compiler.rb:45:in `call'
13: from /Users/gordon/dev/dry-rb/dry-schema/lib/dry/schema/message_compiler.rb:45:in `map'
12: from /Users/gordon/dev/dry-rb/dry-schema/lib/dry/schema/message_compiler.rb:45:in `block in call'
11: from /Users/gordon/dev/dry-rb/dry-schema/lib/dry/schema/message_compiler.rb:50:in `visit'
10: from /Users/gordon/dev/dry-rb/dry-schema/lib/dry/schema/message_compiler.rb:79:in `visit_and'
9: from /Users/gordon/dev/dry-rb/dry-schema/lib/dry/schema/message_compiler.rb:79:in `map'
8: from /Users/gordon/dev/dry-rb/dry-schema/lib/dry/schema/message_compiler.rb:79:in `block in visit_and'
7: from /Users/gordon/dev/dry-rb/dry-schema/lib/dry/schema/message_compiler.rb:50:in `visit'
6: from /Users/gordon/dev/dry-rb/dry-schema/lib/dry/schema/message_compiler.rb:62:in `visit_hint'
5: from /Users/gordon/dev/dry-rb/dry-schema/lib/dry/schema/message_compiler.rb:50:in `visit'
4: from /Users/gordon/dev/dry-rb/dry-schema/lib/dry/schema/message_compiler.rb:137:in `visit_key'
3: from /Users/gordon/dev/dry-rb/dry-schema/lib/dry/schema/message_compiler.rb:50:in `visit'
2: from /Users/gordon/dev/dry-rb/dry-schema/lib/dry/schema/message_compiler.rb:93:in `visit_or'
1: from /Users/gordon/dev/dry-rb/dry-schema/lib/dry/schema/message_compiler.rb:93:in `new'
Things like [:nil, :string]
can lead to confusion when we're dealing with coercible types. Changing the order changes the semantics though for sum types, in general, the semantics remains intact. I think we could have a means for building optional types in a similar way do it for arrays atm:
optional(:foos).value(array[:integer])
optional(:bar).value(optional[:string])
The biggest problem here is coming up with naming 😟 Overloading optional
doesn't look nice.
At the moment you use schemas like:
ClientRequest = Dry::Schema.Params do
required(:request_id) { filled? > str? }
required(:client_logid).filled(:string)
required(:name).filled(:string)
required(:currency).filled(:string)
end
ClientRequest.call(client_logid: 'foo', name: 'Foo', currency: 'EUR')
if you were to mix in something like this into Dry::Schema.Params
:
module Functional
def method_call
method(:call)
end
delegate :>>, :<<, to: :method_call
alias_method :as_proc, :method_call
def [] *args, &block
call(*args, &block)
end
end
you would get full inter-operability with the "ruby lambda ecosystem", ie:
ClientRequest[client_logid: 'foo', name: 'Foo', currency: 'EUR']
(UserValidator >> ApiValidator >> SomeProcOrMethodDoingValidating)[request]
I have been using "functional" mixins with "single public method classes" in my code for years now, I have only good things to say about it and the flexibility it allows you to play with and mix components 👍
This is a nice library and these features would make it ever nicer.
Ability to add new fields when reusing schema. Here, the EmailAddress::Schema::Create
class is reused in another schema with the addition of a new :email_address_id
to the nested hash.
class EmailAddress::Create < ApplicationSchema
define! do
required(:email_address, Types::Email).value(:str?)
optional(:email_label, Types::String).maybe(:str?)
optional(:email_primary, Types::Bool).value(:bool?)
end
end
class EmailAddress::Update < ApplicationSchema
define! do
required(:email_address).schema(EmailAddress::Create) do
required(:email_address_id, Types::String).value(:str?)
end
end
end
The above would be equivalent to
class EmailAddress::Update < ApplicationSchema
define! do
required(:email_address).schema do
required(:email_address, Types::Email).value(:str?)
optional(:email_label, Types::String).maybe(:str?)
optional(:email_primary, Types::Bool).value(:bool?)
required(:email_address_id, Types::String).value(:str?)
end
end
end
The ability to reuse a schema in a flat / non-nested manner (issue #204) following conventions already established, like so:
schema(EmailAddress::Schema::Create)
required(:email_address_id, Types::String).value(:str?)
The above would be equivalent to adding:
required(:email_address, Types::Email).value(:str?)
optional(:email_label, Types::String).maybe(:str?)
optional(:email_primary, Types::Bool).value(:bool?)
required(:email_address_id, Types::String).value(:str?)
When using class inheritance to define a schema class, the ability to override inherited schema methods in a child class. Like so
class EmailAddress::Schema::Base < ApplicationSchema
configure
def valid_primaries?(email_addresses)
true # < -- change
end
end
define! do
required(:email_address, Types::Email).value(:valid_primaries?)
optional(:email_label, Types::String).maybe(:str?)
optional(:email_primary, Types::Bool).value(:bool?)
end
end
class EmailAddress::Schema::Create < EmailAddress::Schema::Base
configure do
def valid_primaries?(email_addresses)
false # < -- change
end
end
define! do
required(:email_addresses, Types::Array).filled.each do
schema(EmailAddress::Schema::Create)
end
end
end
In the above, it'd be awesome if EmailAddress::Schema::Create's updated "valid_primaries?" function was used for the inherited required(:email_address, Types::Email).value(:valid_primaries?)
. When I tried to do something like this, Dry-validation generated an error.
Thanks! Great library!
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.