diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c1cef81 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +### 0.2.0 (unreleased) + +- New features + - New schema types `$enum` and `$one_of` for specifying enumerations and values with multiple + possible schemas ([#5](https://github.com/ShiftForward/frise/pull/5)). +- Bug fixes + - Deal correctly with non-existing schema files in the load path + ([#4](https://github.com/ShiftForward/frise/pull/4)). + +### 0.1.0 (August 10, 2017) + +Initial version. diff --git a/lib/frise/validator.rb b/lib/frise/validator.rb index b2f01ef..e04fedd 100755 --- a/lib/frise/validator.rb +++ b/lib/frise/validator.rb @@ -32,7 +32,9 @@ def add_validation_error(path, msg) def get_full_schema(schema) case schema - when Hash then schema + when Hash then + default_type = schema[:enum] || schema[:one_of] ? 'Object' : 'Hash' + { type: default_type }.merge(schema) when Symbol then { type: 'Object', validate: schema } when Array then if schema.size == 1 @@ -87,6 +89,28 @@ def validate_custom(full_schema, obj, path) true end + def validate_enum(full_schema, obj, path) + if full_schema[:enum] && !full_schema[:enum].include?(obj) + add_validation_error(path, "invalid value #{obj.inspect}. " \ + "Accepted values are #{full_schema[:enum].map(&:inspect).join(', ')}") + return false + end + true + end + + def validate_one_of(full_schema, obj, path) + if full_schema[:one_of] + full_schema[:one_of].each do |schema_opt| + opt_validator = Validator.new(@root, @validators) + opt_validator.validate_object(path, obj, schema_opt) + return true if opt_validator.errors.empty? + end + add_validation_error(path, "#{obj.inspect} does not match any of the possible schemas") + return false + end + true + end + def validate_spec_keys(full_schema, obj, path, processed_keys) full_schema.each do |spec_key, spec_value| next if spec_key.is_a?(Symbol) @@ -122,6 +146,8 @@ def validate_object(path, obj, schema) return unless validate_optional(full_schema, obj, path) return unless validate_type(full_schema, obj, path) return unless validate_custom(full_schema, obj, path) + return unless validate_enum(full_schema, obj, path) + return unless validate_one_of(full_schema, obj, path) processed_keys = Set.new return unless validate_spec_keys(full_schema, obj, path, processed_keys) diff --git a/spec/frise/validator_spec.rb b/spec/frise/validator_spec.rb index c3a5a5a..3818bf7 100644 --- a/spec/frise/validator_spec.rb +++ b/spec/frise/validator_spec.rb @@ -170,6 +170,44 @@ def validators.short_string(_, str) expect(errors).to eq ['At obj: expected a short key, found "objkey0"'] end + it 'should validate correctly enumerations' do + schema = { 'color' => { '$enum' => %w[red green blue] } } + + conf = { 'color' => 'green' } + errors = validate(conf, schema) + expect(errors).to eq [] + + conf = { 'color' => 'car' } + errors = validate(conf, schema) + expect(errors).to eq [ + 'At color: invalid value "car". Accepted values are "red", "green", "blue"' + ] + end + + it 'should validate correctly $one_of choices of schemas' do + schema = { + 'key' => { + '$one_of' => ['String', { 'c' => 'Integer' }] + } + } + + conf = { 'key' => 'abc' } + errors = validate(conf, schema) + expect(errors).to eq [] + + conf = { 'key' => { 'c' => 4 } } + errors = validate(conf, schema) + expect(errors).to eq [] + + conf = { 'key' => 42 } + errors = validate(conf, schema) + expect(errors).to eq ['At key: 42 does not match any of the possible schemas'] + + conf = { 'key' => { 'c' => 'abc' } } + errors = validate(conf, schema) + expect(errors).to eq ['At key: {"c"=>"abc"} does not match any of the possible schemas'] + end + it 'should be able to use complex schemas in their full form' do validators = Object.new def validators.short_string(_, str) @@ -180,7 +218,9 @@ def validators.short_string(_, str) 'opt_int_map' => { '$all_keys' => '$short_string', '$all' => 'Integer', '$optional' => true }, 'opt_sstr' => { '$type' => 'String', '$validate' => '$short_string', '$optional' => true }, 'opt_sstr_arr' => { '$type' => 'Array', '$all' => '$short_string', '$optional' => true }, - 'sstr_arr' => ['$short_string'] + 'sstr_arr' => ['$short_string'], + 'opt_enum' => { '$enum' => %w[a b c], '$optional' => true }, + 'opt_one_of' => { '$one_of' => %w[String Integer], '$optional' => true } } conf = { 'sstr_arr' => ['val'] } @@ -191,7 +231,9 @@ def validators.short_string(_, str) 'opt_int_map' => { 'k0' => 1, 'k1' => 'a', 'mapkey1' => 2 }, 'opt_sstr' => 'abcde', 'opt_sstr_arr' => ['v1', 'v1234', true], - 'sstr_arr' => %w[value val] + 'sstr_arr' => %w[value val], + 'opt_enum' => 'd', + 'opt_one_of' => 4.5 } errors = validate(conf, schema, validators: validators) expect(errors).to eq [ @@ -200,7 +242,9 @@ def validators.short_string(_, str) 'At opt_sstr: expected a short string, found "abcde"', 'At opt_sstr_arr.1: expected a short string, found "v1234"', 'At opt_sstr_arr.2: expected a short string, found true', - 'At sstr_arr.0: expected a short string, found "value"' + 'At sstr_arr.0: expected a short string, found "value"', + 'At opt_enum: invalid value "d". Accepted values are "a", "b", "c"', + 'At opt_one_of: 4.5 does not match any of the possible schemas' ] end