TUTORIAL.md in contracts-0.9 vs TUTORIAL.md in contracts-0.10

- old
+ new

@@ -65,29 +65,45 @@ `Num` is one of the built-in contracts that contracts.ruby comes with. The built-in contracts are in the `Contracts` namespace. The easiest way to use them is to include the `Contracts` module in your class/module. contracts.ruby comes with a lot of built-in contracts, including the following: -* [`Num`](http://www.rubydoc.info/gems/contracts/Contracts/Num) – checks that the argument is `Numeric` -* [`Pos`](http://www.rubydoc.info/gems/contracts/Contracts/Pos) – checks that the argument is a positive number -* [`Neg`](http://www.rubydoc.info/gems/contracts/Contracts/Neg) – checks that the argument is a negative number -* [`Nat`](http://www.rubydoc.info/gems/contracts/Contracts/Nat) – checks that the argument is a natural number -* [`Bool`](http://www.rubydoc.info/gems/contracts/Contracts/Bool) – checks that the argument is `true` or `false` -* [`Any`](http://www.rubydoc.info/gems/contracts/Contracts/Any) – Passes for any argument. Use when the argument has no constraints. -* [`None`](http://www.rubydoc.info/gems/contracts/Contracts/None) – Fails for any argument. Use when the method takes no arguments. -* [`Or`](http://www.rubydoc.info/gems/contracts/Contracts/Or) – passes if any of the given contracts pass, e.g. `Or[Fixnum, Float]` -* [`Xor`](http://www.rubydoc.info/gems/contracts/Contracts/Xor) – passes if exactly one of the given contracts pass, e.g. `Xor[Fixnum, Float]` -* [`And`](http://www.rubydoc.info/gems/contracts/Contracts/And) – passes if all contracts pass, e.g. `And[Fixnum, Float]` -* [`Not`](http://www.rubydoc.info/gems/contracts/Contracts/Not) – passes if all contracts fail for the given argument, e.g. `Not[nil]` -* [`ArrayOf`](http://www.rubydoc.info/gems/contracts/Contracts/ArrayOf) – checks that the argument is an array, and all elements pass the given contract, e.g. `ArrayOf[Num]` -* [`SetOf`](http://www.rubydoc.info/gems/contracts/Contracts/SetOf) – checks that the argument is a set, and all elements pass the given contract, e.g. `SetOf[Num]` -* [`HashOf`](http://www.rubydoc.info/gems/contracts/Contracts/HashOf) – checks that the argument is a hash, and all keys and values pass the given contract, e.g. `HashOf[Symbol => String]` -* [`Maybe`](http://www.rubydoc.info/gems/contracts/Contracts/Maybe) – passes if the argument is `nil`, or if the given contract passes -* [`RespondTo`](http://www.rubydoc.info/gems/contracts/Contracts/RespondTo) – checks that the argument responds to all of the given methods, e.g. `RespondTo[:password, :credit_card]` -* [`Send`](http://www.rubydoc.info/gems/contracts/Contracts/Send) – checks that all named methods return true, e.g. `Send[:valid?]` -* [`Exactly`](http://www.rubydoc.info/gems/contracts/Contracts/Exactly) – checks that the argument has the given type, not accepting sub-classes, e.g. `Exactly[Numeric]`. +* Basic types + * [`Num`](http://www.rubydoc.info/gems/contracts/Contracts/Num) – checks that the argument is `Numeric` + * [`Pos`](http://www.rubydoc.info/gems/contracts/Contracts/Pos) – checks that the argument is a positive number + * [`Neg`](http://www.rubydoc.info/gems/contracts/Contracts/Neg) – checks that the argument is a negative number + * [`Nat`](http://www.rubydoc.info/gems/contracts/Contracts/Nat) – checks that the argument is a natural number (>= 0) + * [`Bool`](http://www.rubydoc.info/gems/contracts/Contracts/Bool) – checks that the argument is `true` or `false` + * [`Any`](http://www.rubydoc.info/gems/contracts/Contracts/Any) – Passes for any argument. Use when the argument has no constraints. + * [`None`](http://www.rubydoc.info/gems/contracts/Contracts/None) – Fails for any argument. Use when the method takes no arguments. +* Logical combinations + * [`Maybe`](http://www.rubydoc.info/gems/contracts/Contracts/Maybe) – specifies that a value _may be_ nil, e.g. `Maybe[String]` (equivalent to `Or[String,nil]`) + * [`Or`](http://www.rubydoc.info/gems/contracts/Contracts/Or) – passes if any of the given contracts pass, e.g. `Or[Fixnum, Float]` + * [`Xor`](http://www.rubydoc.info/gems/contracts/Contracts/Xor) – passes if exactly one of the given contracts pass, e.g. `Xor[Fixnum, Float]` + * [`And`](http://www.rubydoc.info/gems/contracts/Contracts/And) – passes if all contracts pass, e.g. `And[Nat, -> (n) { n.even? }]` + * [`Not`](http://www.rubydoc.info/gems/contracts/Contracts/Not) – passes if all contracts fail for the given argument, e.g. `Not[nil]` + +* Collections + * [`ArrayOf`](http://www.rubydoc.info/gems/contracts/Contracts/ArrayOf) – checks that the argument is an array, and all elements pass the given contract, e.g. `ArrayOf[Num]` + * [`SetOf`](http://www.rubydoc.info/gems/contracts/Contracts/SetOf) – checks that the argument is a set, and all elements pass the given contract, e.g. `SetOf[Num]` + * [`HashOf`](http://www.rubydoc.info/gems/contracts/Contracts/HashOf) – checks that the argument is a hash, and all keys and values pass the given contract, e.g. `HashOf[Symbol => String]` or `HashOf[Symbol,String]` + * [`RangeOf`](http://www.rubydoc.info/gems/contracts/Contracts/RangeOf) – checks that the argument is a range whose elements (#first and #last) pass the given contract, e.g. `RangeOf[Date]` + +* Keyword arguments + * [`KeywordArgs`](http://www.rubydoc.info/gems/contracts/Contracts/KeywordArgs) – checks that the argument is an options hash, and all required keyword arguments are present, and all values pass their respective contracts, e.g. `KeywordArgs[:number => Num, :description => Optional[String]]` + * [`Optional`](http://www.rubydoc.info/gems/contracts/Contracts/Optional) – checks that the keyword argument is either not present or pass the given contract, can not be used outside of `KeywordArgs` contract, e.g. `Optional[Num]` + +* Duck typing + * [`RespondTo`](http://www.rubydoc.info/gems/contracts/Contracts/RespondTo) – checks that the argument responds to all of the given methods, e.g. `RespondTo[:password, :credit_card]` + * [`Send`](http://www.rubydoc.info/gems/contracts/Contracts/Send) – checks that all named methods return a truthy value, e.g. `Send[:valid?]` + +* Miscellaneous + * [`Exactly`](http://www.rubydoc.info/gems/contracts/Contracts/Exactly) – checks that the argument has the given type, not accepting sub-classes, e.g. `Exactly[Numeric]`. + * [`Eq`](http://www.rubydoc.info/gems/contracts/Contracts/Eq) – checks that the argument is precisely equal to the given value, e.g. `Eq[String]` matches the class `String` and not a string instance. + * [`Func`](http://www.rubydoc.info/gems/contracts/Contracts/Func) – specifies the contract for a proc/lambda e.g. `Contract ArrayOf[Num], Func[Num => Num] => ArrayOf[Num]`. See section "Contracts On Functions". + To see all the built-in contracts and their full descriptions, check out the [RDoc](http://rubydoc.info/gems/contracts/Contracts). ## More Examples ### Hello, World @@ -237,10 +253,63 @@ # fails give_largest_value("a" => 1, 2 => 2, c: 3) ``` +### Contracts On Keyword Arguments + +ruby 2.0+, but can be used for normal hashes too, when keyword arguments are +not available + +Lets say you are writing a simple function and require a bunch of keyword arguments: + +```ruby +def connect(host, port:, user:, password:) +``` + +You can of course put `Hash` contract on it: + +```ruby +Contract String, { :port => Num, :user => String, :password => String } => Connection +def connect(host, port:, user:, password:) +``` + +But this will not quite work if you want to have a default values: + +```ruby +Contract String, { :port => Num, :user => String, :password => String } => Connection +def connect(host, port: 5000, user:, password:) + # ... +end + +# No value is passed for port +connect("example.org", user: "me", password: "none") +``` + +Results in: + +``` +ContractError: Contract violation for argument 2 of 2: + Expected: {:port=>Num, :user=>String, :password=>String}, + Actual: {:user=>"me", :password=>"none"} + Value guarded in: Object::connect + With Contract: String, Hash => Connection + At: (irb):12 +``` + +This can be fixed with contract `{ :port => Maybe[Num], ... }`, but that will +allow `nil` to be passed in, which is not the original intent. + +So that is where `KeywordArgs` and `Optional` contracts jump in: + +```ruby +Contract String, KeywordArgs[ :port => Optional[Num], :user => String, :password => String ] => Connection +def connect(host, port: 5000, user:, password:) +``` + +It looks just like the hash contract, but wrapped in `KeywordArgs` contract. Notice the usage of `Optional` contract - this way you specify that `:port` argument is optional. And it will not fail, when you omit this argument, but it will fail when you pass in `nil`. + ### Contracts On Functions Lets say you are writing a simple map function: ```ruby @@ -258,11 +327,11 @@ ```ruby p map([1, 2, 3], lambda { |x| x + 1 }) # works ``` -But suppose you want to have a contract on the Proc too! Suppose you want to make sure that the Proc returns a number. Use the `Func` contract. `Func` takes a contract as it's argument, and uses that contract on the function that you pass in. +But suppose you want to have a contract on the Proc too! Suppose you want to make sure that the Proc returns a number. Use the `Func` contract. `Func` takes a contract as its argument, and uses that contract on the function that you pass in. Here's a `map` function that requires an array of numbers, and a function that takes a number and returns a number: ```ruby Contract ArrayOf[Num], Func[Num => Num] => ArrayOf[Num] @@ -282,10 +351,16 @@ ```ruby p map([1, 2, 3], lambda { |x| x + 1 }) # works p map([1, 2, 3], lambda { |x| "oops" }) # fails, the lambda returns a string. ``` +The above examples showed a method accepting a `Proc` as the last argument, but the same contract works on methods that accept a block: + +```ruby +def map(arr, &block) +``` + NOTE: This is not valid: ```ruby Contract ArrayOf[Num], Func => ArrayOf[Num] def map(arr, &func) @@ -446,10 +521,40 @@ :contracts => the contract object } If your failure callback returns `false`, the method that the contract is guarding will not be called (the default behaviour). +## Providing your own custom validators + +This can be done with `Contract.override_validator`: + +```ruby +# Make contracts accept all RSpec doubles +Contract.override_validator(:class) do |contract| + lambda do |arg| + arg.is_a?(RSpec::Mocks::Double) || + arg.is_a?(contract) + end +end +``` + +The block you provide should always return lambda accepting one argument - validated argument. Block itself accepts contract as an argument. + +Possible validator overrides: + +- `override_validator(MyCustomContract)` - allows to add some special behaviour for custom contracts, +- `override_validator(Proc)` - e.g. `lambda { true }`, +- `override_validator(Array)` - e.g. `[Num, String]`, +- `override_validator(Hash)` - e.g. `{ :a => Num, :b => String }`, +- `override_validator(Contracts::Args)` - e.g. `Args[Num]`, +- `override_validator(Contracts::Func)` - e.g. `Func[Num => Num]`, +- `override_validator(:valid)` - allows to override how contracts that respond to `:valid?` are handled, +- `override_validator(:class)` - allows to override how class/module contract constants are handled, +- `override_validator(:default)` - otherwise, raw value contracts. + +Default validators can be found here: [lib/contracts/validators.rb](https://github.com/egonSchiele/contracts.ruby/blob/master/lib/contracts/validators.rb). + ## Disabling contracts If you want to disable contracts, set the `NO_CONTRACTS` environment variable. This will disable contracts and you won't have a performance hit. Pattern matching will still work if you disable contracts in this way! With NO_CONTRACTS only pattern-matching contracts are defined. ## Method overloading @@ -508,16 +613,15 @@ This is because the first contract eliminated the possibility of `age` being less than 12. However, the simpler contract is less explicit; you may want to "spell out" the age condition for clarity, especially if the method is overloaded with many contracts. ## Contracts in modules -To use contracts on module you need to include both `Contracts` and `Contracts::Modules` into it: +Usage is the same as contracts in classes: ```ruby module M include Contracts - include Contracts::Modules Contract String => String def self.parse # do some hard parsing end @@ -559,9 +663,20 @@ Value guarded in: MyBirthday::silly_next_day! At: main.rb:9 ``` Which means, that after `#silly_next_day!` all checks specified in `invariant` statement will be verified, and if at least one fail, then invariant violation error will be raised. + +## Using contracts within your own code + +contracts.ruby is obviously designed to check method parameters and return values. But if you want to check whether some other data obeys a contract, you can use `Contract.valid?(value, contract)`. For instance: + +```ruby +data = parse(user_input) +unless Contract.valid?(data, HashOf[String,Nat]) + raise UserInputError.new(user_input) +end +``` ## Auto-generate documentation using contracts If you are generating documentation for your code with [YARD](http://yardoc.org/), check out [yard-contracts](https://github.com/sfcgeorge/yard-contracts). It will automatically annotate your functions with contracts information. Instead of documenting each parameter for a function yourself, you can just add a contract and yard-contracts will generate the documentation for you!