README_V3.md in schemacop-3.0.0.rc1 vs README_V3.md in schemacop-3.0.0.rc2

- old
+ new

@@ -1,17 +1,13 @@ # Schemacop schema V3 -Please note that Schemacop v3 is still a work in progress, especially the documentation. +## Table of Contents -Use at your own discretion. - -# Table of Contents -1. [Introduction](#Introduction) -2. [Validation](#validation) -3. [Exceptions](#exceptions) -4. [Generic Keywords](#generic-keywords) -5. [Nodes](#nodes) +1. [Validation](#validation) +2. [Exceptions](#exceptions) +3. [Generic Keywords](#generic-keywords) +4. [Nodes](#nodes) 1. [String](#string) 2. [Integer](#integer) 3. [Number](#number) 4. [Symbol](#symbol) 5. [Boolean](#boolean) @@ -21,17 +17,13 @@ 9. [AllOf](#allOf) 10. [AnyOf](#anyOf) 11. [OneOf](#oneOf) 12. [IsNot](#isNot) 13. [Reference](#reference) -6. [Context](#context) -7. [External schemas](#external-schemas) +5. [Context](#context) +6. [External schemas](#external-schemas) -## Introduction - -TODO: Write short section about using schemacop V3 - ## Validation Using schemacop, you can either choose to validate the data either using the graceful `validate` method, or the bang variant, `validate!`. @@ -72,24 +64,126 @@ schema.validate!('Foo') # => Schemacop::Exceptions::ValidationError: /: String does not match format "date". ``` ## Exceptions -TODO: Describe the exceptions raised by schemacop +Schemacop can raise the following exceptions: -`Schemacop::Exceptions::ValidationError` -`Schemacop::Exceptions::InvalidSchemaError` +* `Schemacop::Exceptions::ValidationError`: This exception is raised when the `validate!` + method is used, and the data that was passed in is invalid. The exception message contains + additional informations why the validation failed. + Example: + + ```ruby + schema = Schemacop::Schema3.new :hash do + int! :foo + end + + schema.validate!(foo: 'bar') + # => Schemacop::Exceptions::ValidationError: /foo: Invalid type, expected "integer". + ``` + +* `Schemacop::Exceptions::InvalidSchemaError`: This exception is raised when the schema + itself is not valid. The exception message contains additional informations why the + validation failed. + + Example: + + ```ruby + Schemacop::Schema3.new :hash do + int! + end + + # => Schemacop::Exceptions::InvalidSchemaError: Child nodes must have a name. + ``` + ## Generic Keywords -TODO: Complete this +The nodes in Schemacop v3 also support generic keywords, similar to JSON schema: -* enum -* title -* description -* examples +* `title`: Short string, should be self-explanatory +* `description`: Description of the schema +* `examples`: Here, you can provide examples which will be valid for the schema +* `enum`: Here, you may enumerate values which will be valid, if the provided + value is not in the array, the validation will fail +* `default`: You may provide a default value for items that will be set if the + value is not given + +The three keywords `title`, `description` and `examples` aren't used for validation, +but can be used to document the schema. They will be included in the JSON output +when you use the `as_json` method: + +```ruby +schema = Schemacop::Schema3.new :hash do + str! :name, title: 'Name', description: 'Holds the name of the user', examples: ['Joe', 'Anna'] +end + +schema.as_json + +# => {"properties"=>{"name"=>{"type"=>"string", "title"=>"Name", "examples"=>["Joe", "Anna"], "description"=>"Holds the name of the user"}}, "additionalProperties"=>false, "required"=>["name"], "type"=>"object"} +``` + +The `enum` keyword can be used to only allow a subset of values: + +```ruby +schema = Schemacop::Schema3.new :string, enum: ['foo', 'bar'] + +schema.validate!('foo') # => "foo" +schema.validate!('bar') # => "bar" +schema.validate!('baz') # => Schemacop::Exceptions::ValidationError: /: Value not included in enum ["foo", "bar"]. +``` + +Please note, that you can also specify values in the enum that are not valid for +the schema. This means that the validation will still fail: + +```ruby +schema = Schemacop::Schema3.new :string, enum: ['foo', 'bar', 42] + +schema.validate!('foo') # => "foo" +schema.validate!('bar') # => "bar" +schema.validate!(42) # => Schemacop::Exceptions::ValidationError: /: Invalid type, expected "string". +``` + +The enum will also be provided in the json output: + +```ruby +schema = Schemacop::Schema3.new :string, enum: ['foo', 'bar'] + +schema.as_json +# => {"type"=>"string", "enum"=>["foo", "bar", 42]} +``` + +And finally, the `default` keyword lets you set a default value to use when no +value is provided: + +```ruby +schema = Schemacop::Schema3.new :string, default: 'Schemacop' + +schema.validate!('foo') # => "foo" +schema.validate!(nil) # => "Schemacop" +``` + +The default value will also be provided in the json output: + +```ruby +schema = Schemacop::Schema3.new :string, default: 'Schemacop' + +schema.as_json +# => {"type"=>"string", "default"=>"Schemacop"} +``` + +Note that the default value you use is also validated against the schema: + +```ruby +schema = Schemacop::Schema3.new :string, default: 42 + +schema.validate!('foo') # => "foo" +schema.validate!(nil) # => Schemacop::Exceptions::ValidationError: /: Invalid type, expected "string". +``` + ## Nodes ### String Type: `:string`\ @@ -521,15 +615,65 @@ str! :foo # Is a required property int? :bar # Is an optional property end schema.validate!({}) # => Schemacop::Exceptions::ValidationError: /foo: Value must be given. -schema.validate!({foo: 'str'}) # => {:foo=>"str"} -schema.validate!({foo: 'str', bar: 42}) # => {:foo=>"str", :bar=>42} +schema.validate!({foo: 'str'}) # => {"foo"=>"str"} +schema.validate!({foo: 'str', bar: 42}) # => {"foo"=>"str", "bar"=>42} schema.validate!({bar: 42}) # => Schemacop::Exceptions::ValidationError: /foo: Value must be given. ``` +The name of the properties may either be a string or a symbol, and you can pass +in the property either identified by a symbol or a string: + +The following two schemas are equal: + +```ruby +schema = Schemacop::Schema3.new :hash do + int! :foo +end + +schema.validate!(foo: 42) # => {"foo"=>42} +schema.validate!('foo' => 42) # => {"foo"=>42} + +schema = Schemacop::Schema3.new :hash do + int! 'foo' +end + +schema.validate!(foo: 42) # => {"foo"=>42} +schema.validate!('foo' => 42) # => {"foo"=>42} +``` + +The result in both cases will be a +[HashWithIndifferentAccess](https://api.rubyonrails.org/classes/ActiveSupport/HashWithIndifferentAccess.html), +which means that you can access the data in the hash with the symbol as well +as the string representation: + +```ruby +schema = Schemacop::Schema3.new :hash do + int! :foo +end + +result = schema.validate!(foo: 42) + +result.class # => ActiveSupport::HashWithIndifferentAccess +result[:foo] # => 42 +result['foo'] # 42 +``` + +Please note, that if you specify the value twice in the data you want to validate, +once with the key being a symbol and once being a string, Schemacop will raise an +error: + +```ruby +schema = Schemacop::Schema3.new :hash do + int! :foo +end + +schema.validate!(foo: 42, 'foo' => 43) # => Schemacop::Exceptions::ValidationError: /: Has 1 ambiguous properties: [:foo]. +``` + ##### Pattern properties In addition to symbols, property keys can also be a regular expression. Here, you may only use the optional `?` suffix for the property. This allows any property, which matches the type and the name of the property matches the @@ -541,12 +685,12 @@ # name starts with `id_`. int? /^id_.*$/ end schema.validate!({}) # => {} -schema.validate!({id_foo: 1}) # => {:id_foo=>1} -schema.validate!({id_foo: 1, id_bar: 2}) # => {:id_foo=>1, :id_bar=>2} +schema.validate!({id_foo: 1}) # => {"id_foo"=>1} +schema.validate!({id_foo: 1, id_bar: 2}) # => {"id_foo"=>1, "id_bar"=>2} schema.validate!({foo: 3}) # => Schemacop::Exceptions::ValidationError: /: Obsolete property "foo". ``` ##### Additional properties & property names @@ -560,11 +704,11 @@ ```ruby # This schema will accept any additional properties schema = Schemacop::Schema3.new :hash, additional_properties: true schema.validate!({}) # => {} -schema.validate!({foo: :bar, baz: 42}) # => {:foo=>:bar, :baz=>42} +schema.validate!({foo: :bar, baz: 42}) # => {"foo"=>:bar, "baz"=>42} ``` Using the DSL method `add` in the hash-node's body however, you can specify an additional schema to which additional properties must adhere: @@ -576,36 +720,36 @@ # Allow any additional properties besides `id`, but their value must be a # string. add :string end -schema.validate!({id: 1}) # => {:id=>1} -schema.validate!({id: 1, foo: 'bar'}) # => {:foo=>"bar", :id=>1} +schema.validate!({id: 1}) # => {"id"=>1} +schema.validate!({id: 1, foo: 'bar'}) # => {"id"=>1, "foo"=>"bar"} schema.validate!({id: 1, foo: 42}) # => Schemacop::Exceptions::ValidationError: /foo: Invalid type, expected "string". ``` Using the option `property_names`, you can additionaly specify a pattern that any additional property **keys** must adhere to: ```ruby # The following schema allows any number of properties, but all keys must # consist of downcase letters from a-z. -schema = Schemacop::Schema3.new :hash, additional_properties: :true, property_names: '^[a-z]+$' +schema = Schemacop::Schema3.new :hash, additional_properties: true, property_names: '^[a-z]+$' schema.validate!({}) # => {} -schema.validate!({foo: 123}) # => {:foo=>123} -schema.validate!({Foo: 'bar'}) # => Schemacop::Exceptions::ValidationError: /: Property name :Foo does not match "^[a-z]+$". +schema.validate!({foo: 123}) # => {"foo"=>123} +schema.validate!({Foo: 'bar'}) # => Schemacop::Exceptions::ValidationError: /: Property name "Foo" does not match "^[a-z]+$". # The following schema allows any number of properties, but all keys must # consist of downcase letters from a-z AND the properties must be arrays. schema = Schemacop::Schema3.new :hash, additional_properties: true, property_names: '^[a-z]+$' do add :array end schema.validate!({}) # => {} -schema.validate!({foo: [1, 2, 3]}) # => {:foo=>[1, 2, 3]} +schema.validate!({foo: [1, 2, 3]}) # => {"foo"=>[1, 2, 3]} schema.validate!({foo: :bar}) # => Schemacop::Exceptions::ValidationError: /foo: Invalid type, expected "array". schema.validate!({Foo: :bar}) # => Schemacop::Exceptions::ValidationError: /: Property name :Foo does not match "^[a-z]+$". /Foo: Invalid type, expected "array". ``` ##### Dependencies @@ -625,11 +769,11 @@ dep :credit_card, :billing_address, :phone_number dep :billing_address, :credit_card end schema.validate!({}) # => Schemacop::Exceptions::ValidationError: /name: Value must be given. -schema.validate!({name: 'Joe Doe'}) # => {:name=>"Joe Doe"} +schema.validate!({name: 'Joe Doe'}) # => {"name"=>"Joe Doe"} schema.validate!({ name: 'Joe Doe', billing_address: 'Street 42' }) # => Schemacop::Exceptions::ValidationError: /: Missing property "credit_card" because "billing_address" is given. @@ -644,19 +788,19 @@ name: 'Joe Doe', billing_address: 'Street 42', phone_number: '000-000-00-00', credit_card: 'XXXX XXXX XXXX XXXX X' }) -# => {:name=>"Joe Doe", :credit_card=>"XXXX XXXX XXXX XXXX X", :billing_address=>"Street 42", :phone_number=>"000-000-00-00"} +# => {"name"=>"Joe Doe", "credit_card"=>"XXXX XXXX XXXX XXXX X", "billing_address"=>"Street 42", "phone_number"=>"000-000-00-00"} ``` ### Object Type: `:object`\ DSL: `obj` -The object type represents a ruby `Object`. Please note that the `as_json? method +The object type represents a ruby `Object`. Please note that the `as_json` method on nodes of this type will just return `{}` (an empty JSON object), as there isn't a useful way to represent a ruby object without conflicting with the `Hash` type. If you want to represent an JSON object, you should use the `Hash` node. In the most basic form, this node will accept anything: @@ -882,11 +1026,11 @@ location: 'Washington DC', country: 'USA' } }) -# => {:shipping_address=>{:street=>"Example Street 42", :zip_code=>"12345", :location=>"London", :country=>"United Kingdom"}, :billing_address=>{:street=>"Main St.", :zip_code=>"54321", :location=>"Washington DC", :country=>"USA"}} +# => {"shipping_address"=>{"street"=>"Example Street 42", "zip_code"=>"12345", "location"=>"London", "country"=>"United Kingdom"}, "billing_address"=>{"street"=>"Main St.", "zip_code"=>"54321", "location"=>"Washington DC", "country"=>"USA"}} ``` Note that if you use the reference node with the long type name `reference`, e.g. in an array, you need to specify the "name" of the schema in the `path` option: @@ -900,11 +1044,11 @@ list :reference, path: :User end schema.validate!([]) # => [] -schema.validate!([{first_name: 'Joe', last_name: 'Doe'}]) # => [{:first_name=>"Joe", :last_name=>"Doe"}] +schema.validate!([{first_name: 'Joe', last_name: 'Doe'}]) # => [{"first_name"=>"Joe", "last_name"=>"Doe"}] schema.validate!([id: 42, first_name: 'Joe']) # => Schemacop::Exceptions::ValidationError: /[0]/last_name: Value must be given. /[0]: Obsolete property "id". ``` ## Context @@ -938,11 +1082,11 @@ # Validate the data in the context we defined before, where we need the first_name # and last_name of a person, as well as an optional info hash with the born_at date # of the person. Schemacop.with_context context do schema.validate!({first_name: 'Joe', last_name: 'Doe', info: { born_at: '1980-01-01'} }) - # => {:first_name=>"Joe", :last_name=>"Doe", :info=>{:born_at=>Tue, 01 Jan 1980}} + # => {"first_name"=>"Joe", "last_name"=>"Doe", "info"=>{"born_at"=>Tue, 01 Jan 1980}} end # Now we might want another context, where the person is more anonymous, and as # such, we need another schema other_context = Schemacop::V3::Context.new @@ -959,11 +1103,11 @@ # => Schemacop::Exceptions::ValidationError: /nickname: Value must be given. # /: Obsolete property "first_name". # /: Obsolete property "last_name". # /: Obsolete property "info". - schema.validate!({nickname: 'J.'}) # => {:nickname=>"J."} + schema.validate!({nickname: 'J.'}) # => {"nickname"=>"J."} end ``` As one can see, we validated the data against the same schema, but because we defined the referenced schemas differently in the two contexts, we were able @@ -1030,16 +1174,16 @@ schema = Schemacop::Schema3.new :hash do ref! :usr, :user end schema.validate!({usr: {first_name: 'Joe', last_name: 'Doe'}}) - # => {:usr=>{:first_name=>"Joe", :last_name=>"Doe"}} + # => {"usr"=>{"first_name"=>"Joe", "last_name"=>"Doe"}} schema.validate!({usr: {first_name: 'Joe', last_name: 'Doe', groups: []}}) - # => {:usr=>{:first_name=>"Joe", :last_name=>"Doe", :groups=>[]}} + # => {"usr"=>{"first_name"=>"Joe", "last_name"=>"Doe", "groups"=>[]}} schema.validate!({usr: {first_name: 'Joe', last_name: 'Doe', groups: [{name: 'foo'}, {name: 'bar'}]}}) - # => {:usr=>{:first_name=>"Joe", :last_name=>"Doe", :groups=>[{:name=>"foo"}, {:name=>"bar"}]}} + # => {"usr"=>{"first_name"=>"Joe", "last_name"=>"Doe", "groups"=>[{"name"=>"foo"}, {"name"=>"bar"}]}} ``` ### Non-Rails applications Usage in non-Rails applications is the same as with usage in Rails applications, \ No newline at end of file