diff --git a/lib/contracts.rb b/lib/contracts.rb index b71b45c..2e6e9ea 100644 --- a/lib/contracts.rb +++ b/lib/contracts.rb @@ -108,7 +108,7 @@ def pretty_contract c def to_s args = args_contracts.map { |c| pretty_contract(c) }.join(", ") ret = pretty_contract(ret_contract) - ("#{args} => #{ret}").gsub("Contracts::", "") + ("#{args} => #{ret}").gsub("Contracts::Builtin::", "") end # Given a hash, prints out a failure message. diff --git a/lib/contracts/builtin_contracts.rb b/lib/contracts/builtin_contracts.rb index 2ef4158..1430d6a 100644 --- a/lib/contracts/builtin_contracts.rb +++ b/lib/contracts/builtin_contracts.rb @@ -19,460 +19,465 @@ # The contract is Contract Num, Num, Num. # That says that the +add+ function takes two numbers and returns a number. module Contracts - # Check that an argument is +Numeric+. - class Num - def self.valid? val - val.is_a? Numeric - end - end - - # Check that an argument is a positive number. - class Pos - def self.valid? val - val && val.is_a?(Numeric) && val > 0 - end - end - - # Check that an argument is a negative number. - class Neg - def self.valid? val - val && val.is_a?(Numeric) && val < 0 + module Builtin + # Check that an argument is +Numeric+. + class Num + def self.valid? val + val.is_a? Numeric + end end - end - # Check that an argument is a natural number. - class Nat - def self.valid? val - val && val.is_a?(Integer) && val >= 0 + # Check that an argument is a positive number. + class Pos + def self.valid? val + val && val.is_a?(Numeric) && val > 0 + end end - end - # Passes for any argument. - class Any - def self.valid? val - true + # Check that an argument is a negative number. + class Neg + def self.valid? val + val && val.is_a?(Numeric) && val < 0 + end end - end - # Fails for any argument. - class None - def self.valid? val - false + # Check that an argument is a natural number. + class Nat + def self.valid? val + val && val.is_a?(Integer) && val >= 0 + end end - end - # Use this when you are writing your own contract classes. - # Allows your contract to be called with [] instead of .new: - # - # Old: Or.new(param1, param2) - # - # New: Or[param1, param2] - # - # Of course, .new still works. - class CallableClass - include ::Contracts::Formatters - def self.[](*vals) - new(*vals) - end - end - - # Takes a variable number of contracts. - # The contract passes if any of the contracts pass. - # Example: Or[Fixnum, Float] - class Or < CallableClass - def initialize(*vals) - @vals = vals + # Passes for any argument. + class Any + def self.valid? val + true + end end - def valid?(val) - @vals.any? do |contract| - res, _ = Contract.valid?(val, contract) - res + # Fails for any argument. + class None + def self.valid? val + false end end - def to_s - @vals[0, @vals.size-1].map do |x| - InspectWrapper.create(x) - end.join(", ") + " or " + InspectWrapper.create(@vals[-1]).to_s + # Use this when you are writing your own contract classes. + # Allows your contract to be called with [] instead of .new: + # + # Old: Or.new(param1, param2) + # + # New: Or[param1, param2] + # + # Of course, .new still works. + class CallableClass + include ::Contracts::Formatters + def self.[](*vals) + new(*vals) + end end - end - # Takes a variable number of contracts. - # The contract passes if exactly one of those contracts pass. - # Example: Xor[Fixnum, Float] - class Xor < CallableClass - def initialize(*vals) - @vals = vals - end + # Takes a variable number of contracts. + # The contract passes if any of the contracts pass. + # Example: Or[Fixnum, Float] + class Or < CallableClass + def initialize(*vals) + @vals = vals + end - def valid?(val) - results = @vals.map do |contract| - res, _ = Contract.valid?(val, contract) - res + def valid?(val) + @vals.any? do |contract| + res, _ = Contract.valid?(val, contract) + res + end end - results.count(true) == 1 - end - def to_s - @vals[0, @vals.size-1].map do |x| - InspectWrapper.create(x) - end.join(", ") + " xor " + InspectWrapper.create(@vals[-1]).to_s + def to_s + @vals[0, @vals.size-1].map do |x| + InspectWrapper.create(x) + end.join(", ") + " or " + InspectWrapper.create(@vals[-1]).to_s + end end - end - # Takes a variable number of contracts. - # The contract passes if all contracts pass. - # Example: And[Fixnum, Float] - class And < CallableClass - def initialize(*vals) - @vals = vals - end + # Takes a variable number of contracts. + # The contract passes if exactly one of those contracts pass. + # Example: Xor[Fixnum, Float] + class Xor < CallableClass + def initialize(*vals) + @vals = vals + end - def valid?(val) - @vals.all? do |contract| - res, _ = Contract.valid?(val, contract) - res + def valid?(val) + results = @vals.map do |contract| + res, _ = Contract.valid?(val, contract) + res + end + results.count(true) == 1 end - end - def to_s - @vals[0, @vals.size-1].map do |x| - InspectWrapper.create(x) - end.join(", ") + " and " + InspectWrapper.create(@vals[-1]).to_s + def to_s + @vals[0, @vals.size-1].map do |x| + InspectWrapper.create(x) + end.join(", ") + " xor " + InspectWrapper.create(@vals[-1]).to_s + end end - end - # Takes a variable number of method names as symbols. - # The contract passes if the argument responds to all - # of those methods. - # Example: RespondTo[:password, :credit_card] - class RespondTo < CallableClass - def initialize(*meths) - @meths = meths - end + # Takes a variable number of contracts. + # The contract passes if all contracts pass. + # Example: And[Fixnum, Float] + class And < CallableClass + def initialize(*vals) + @vals = vals + end - def valid?(val) - @meths.all? do |meth| - val.respond_to? meth + def valid?(val) + @vals.all? do |contract| + res, _ = Contract.valid?(val, contract) + res + end end - end - def to_s - "a value that responds to #{@meths.inspect}" + def to_s + @vals[0, @vals.size-1].map do |x| + InspectWrapper.create(x) + end.join(", ") + " and " + InspectWrapper.create(@vals[-1]).to_s + end end - end - # Takes a variable number of method names as symbols. - # Given an argument, all of those methods are called - # on the argument one by one. If they all return true, - # the contract passes. - # Example: Send[:valid?] - class Send < CallableClass - def initialize(*meths) - @meths = meths - end + # Takes a variable number of method names as symbols. + # The contract passes if the argument responds to all + # of those methods. + # Example: RespondTo[:password, :credit_card] + class RespondTo < CallableClass + def initialize(*meths) + @meths = meths + end - def valid?(val) - @meths.all? do |meth| - val.send(meth) + def valid?(val) + @meths.all? do |meth| + val.respond_to? meth + end end - end - def to_s - "a value that returns true for all of #{@meths.inspect}" + def to_s + "a value that responds to #{@meths.inspect}" + end end - end - # Takes a class +A+. If argument is object of type +A+, the contract passes. - # If it is a subclass of A (or not related to A in any way), it fails. - # Example: Exactly[Numeric] - class Exactly < CallableClass - def initialize(cls) - @cls = cls - end + # Takes a variable number of method names as symbols. + # Given an argument, all of those methods are called + # on the argument one by one. If they all return true, + # the contract passes. + # Example: Send[:valid?] + class Send < CallableClass + def initialize(*meths) + @meths = meths + end - def valid?(val) - val.class == @cls - end + def valid?(val) + @meths.all? do |meth| + val.send(meth) + end + end - def to_s - "exactly #{@cls.inspect}" + def to_s + "a value that returns true for all of #{@meths.inspect}" + end end - end - # Takes a list of values, e.g. +[:a, :b, :c]+. If argument is included in - # the list, the contract passes. - # - # Example: Enum[:a, :b, :c]? - class Enum < CallableClass - def initialize(*vals) - @vals = vals - end + # Takes a class +A+. If argument is object of type +A+, the contract passes. + # If it is a subclass of A (or not related to A in any way), it fails. + # Example: Exactly[Numeric] + class Exactly < CallableClass + def initialize(cls) + @cls = cls + end - def valid?(val) - @vals.include? val - end - end + def valid?(val) + val.class == @cls + end - # Takes a value +v+. If the argument is +.equal+ to +v+, the contract passes, - # otherwise the contract fails. - # Example: Eq[Class] - class Eq < CallableClass - def initialize(value) - @value = value + def to_s + "exactly #{@cls.inspect}" + end end - def valid?(val) - @value.equal?(val) - end + # Takes a list of values, e.g. +[:a, :b, :c]+. If argument is included in + # the list, the contract passes. + # + # Example: Enum[:a, :b, :c]? + class Enum < CallableClass + def initialize(*vals) + @vals = vals + end - def to_s - "to be equal to #{@value.inspect}" + def valid?(val) + @vals.include? val + end end - end - # Takes a variable number of contracts. The contract - # passes if all of those contracts fail for the given argument. - # Example: Not[nil] - class Not < CallableClass - def initialize(*vals) - @vals = vals - end + # Takes a value +v+. If the argument is +.equal+ to +v+, the contract passes, + # otherwise the contract fails. + # Example: Eq[Class] + class Eq < CallableClass + def initialize(value) + @value = value + end - def valid?(val) - @vals.all? do |contract| - res, _ = Contract.valid?(val, contract) - !res + def valid?(val) + @value.equal?(val) end - end - def to_s - "a value that is none of #{@vals.inspect}" + def to_s + "to be equal to #{@value.inspect}" + end end - end - # @private - # Takes a collection(responds to :each) type and a contract. - # The related argument must be of specified collection type. - # Checks the contract against every element of the collection. - # If it passes for all elements, the contract passes. - # Example: CollectionOf[Array, Num] - class CollectionOf < CallableClass - def initialize(collection_class, contract) - @collection_class = collection_class - @contract = contract - end + # Takes a variable number of contracts. The contract + # passes if all of those contracts fail for the given argument. + # Example: Not[nil] + class Not < CallableClass + def initialize(*vals) + @vals = vals + end - def valid?(vals) - return false unless vals.is_a?(@collection_class) - vals.all? do |val| - res, _ = Contract.valid?(val, @contract) - res + def valid?(val) + @vals.all? do |contract| + res, _ = Contract.valid?(val, contract) + !res + end end - end - def to_s - "a collection #{@collection_class} of #{@contract}" + def to_s + "a value that is none of #{@vals.inspect}" + end end - class Factory - def initialize(collection_class, &before_new) + # @private + # Takes a collection(responds to :each) type and a contract. + # The related argument must be of specified collection type. + # Checks the contract against every element of the collection. + # If it passes for all elements, the contract passes. + # Example: CollectionOf[Array, Num] + class CollectionOf < CallableClass + def initialize(collection_class, contract) @collection_class = collection_class - @before_new = before_new + @contract = contract end - def new(contract) - @before_new && @before_new.call - CollectionOf.new(@collection_class, contract) + def valid?(vals) + return false unless vals.is_a?(@collection_class) + vals.all? do |val| + res, _ = Contract.valid?(val, @contract) + res + end end - alias_method :[], :new - end - end + def to_s + "a collection #{@collection_class} of #{@contract}" + end - # Takes a contract. The related argument must be an array. - # Checks the contract against every element of the array. - # If it passes for all elements, the contract passes. - # Example: ArrayOf[Num] - ArrayOf = CollectionOf::Factory.new(Array) - - # Takes a contract. The related argument must be a set. - # Checks the contract against every element of the set. - # If it passes for all elements, the contract passes. - # Example: SetOf[Num] - SetOf = CollectionOf::Factory.new(Set) - - # Used for *args (variadic functions). Takes a contract - # and uses it to validate every element passed in - # through *args. - # Example: Args[Or[String, Num]] - class Args < CallableClass - attr_reader :contract - def initialize(contract) - @contract = contract - end + class Factory + def initialize(collection_class, &before_new) + @collection_class = collection_class + @before_new = before_new + end - def to_s - "Args[#{@contract}]" - end - end + def new(contract) + @before_new && @before_new.call + CollectionOf.new(@collection_class, contract) + end - class Bool - def self.valid? val - val.is_a?(TrueClass) || val.is_a?(FalseClass) + alias_method :[], :new + end end - end - # Use this to specify a Range object of a particular datatype. - # Example: RangeOf[Nat], RangeOf[Date], ... - class RangeOf < CallableClass - def initialize(contract) - @contract = contract - end + # Takes a contract. The related argument must be an array. + # Checks the contract against every element of the array. + # If it passes for all elements, the contract passes. + # Example: ArrayOf[Num] + ArrayOf = CollectionOf::Factory.new(Array) + + # Takes a contract. The related argument must be a set. + # Checks the contract against every element of the set. + # If it passes for all elements, the contract passes. + # Example: SetOf[Num] + SetOf = CollectionOf::Factory.new(Set) + + # Used for *args (variadic functions). Takes a contract + # and uses it to validate every element passed in + # through *args. + # Example: Args[Or[String, Num]] + class Args < CallableClass + attr_reader :contract + def initialize(contract) + @contract = contract + end - def valid?(val) - val.is_a?(Range) && - Contract.valid?(val.first, @contract) && - Contract.valid?(val.last, @contract) + def to_s + "Args[#{@contract}]" + end end - def to_s - "a range of #{@contract}" + class Bool + def self.valid? val + val.is_a?(TrueClass) || val.is_a?(FalseClass) + end end - end - # Use this to specify the Hash characteristics. Takes two contracts, - # one for hash keys and one for hash values. - # Example: HashOf[Symbol, String] - class HashOf < CallableClass - INVALID_KEY_VALUE_PAIR = "You should provide only one key-value pair to HashOf contract" - - def initialize(key, value = nil) - if value - @key = key - @value = value - else - validate_hash(key) - @key = key.keys.first - @value = key[@key] + # Use this to specify a Range object of a particular datatype. + # Example: RangeOf[Nat], RangeOf[Date], ... + class RangeOf < CallableClass + def initialize(contract) + @contract = contract end - end - def valid?(hash) - return false unless hash.is_a?(Hash) - keys_match = hash.keys.map { |k| Contract.valid?(k, @key) }.all? - vals_match = hash.values.map { |v| Contract.valid?(v, @value) }.all? + def valid?(val) + val.is_a?(Range) && + Contract.valid?(val.first, @contract) && + Contract.valid?(val.last, @contract) + end - [keys_match, vals_match].all? + def to_s + "a range of #{@contract}" + end end - def to_s - "Hash<#{@key}, #{@value}>" - end + # Use this to specify the Hash characteristics. Takes two contracts, + # one for hash keys and one for hash values. + # Example: HashOf[Symbol, String] + class HashOf < CallableClass + INVALID_KEY_VALUE_PAIR = "You should provide only one key-value pair to HashOf contract" + + def initialize(key, value = nil) + if value + @key = key + @value = value + else + validate_hash(key) + @key = key.keys.first + @value = key[@key] + end + end - private + def valid?(hash) + return false unless hash.is_a?(Hash) + keys_match = hash.keys.map { |k| Contract.valid?(k, @key) }.all? + vals_match = hash.values.map { |v| Contract.valid?(v, @value) }.all? - def validate_hash(hash) - fail ArgumentError, INVALID_KEY_VALUE_PAIR unless hash.count == 1 - end - end + [keys_match, vals_match].all? + end - # Use this for specifying contracts for keyword arguments - # Example: KeywordArgs[ e: Range, f: Optional[Num] ] - class KeywordArgs < CallableClass - def initialize(options) - @options = options - end + def to_s + "Hash<#{@key}, #{@value}>" + end + + private - def valid?(hash) - return false unless hash.keys - options.keys == [] - options.all? do |key, contract| - Optional._valid?(hash, key, contract) + def validate_hash(hash) + fail ArgumentError, INVALID_KEY_VALUE_PAIR unless hash.count == 1 end end - def to_s - "KeywordArgs[#{options}]" - end + # Use this for specifying contracts for keyword arguments + # Example: KeywordArgs[ e: Range, f: Optional[Num] ] + class KeywordArgs < CallableClass + def initialize(options) + @options = options + end - def inspect - to_s - end + def valid?(hash) + return false unless hash.keys - options.keys == [] + options.all? do |key, contract| + Optional._valid?(hash, key, contract) + end + end - private + def to_s + "KeywordArgs[#{options}]" + end - attr_reader :options - end + def inspect + to_s + end - # Use this for specifying optional keyword argument - # Example: Optional[Num] - class Optional < CallableClass - UNABLE_TO_USE_OUTSIDE_OF_OPT_HASH = - "Unable to use Optional contract outside of KeywordArgs contract" + private - def self._valid?(hash, key, contract) - return Contract.valid?(hash[key], contract) unless contract.is_a?(Optional) - contract.within_opt_hash! - !hash.key?(key) || Contract.valid?(hash[key], contract) + attr_reader :options end - def initialize(contract) - @contract = contract - @within_opt_hash = false - end + # Use this for specifying optional keyword argument + # Example: Optional[Num] + class Optional < CallableClass + UNABLE_TO_USE_OUTSIDE_OF_OPT_HASH = + "Unable to use Optional contract outside of KeywordArgs contract" - def within_opt_hash! - @within_opt_hash = true - self - end + def self._valid?(hash, key, contract) + return Contract.valid?(hash[key], contract) unless contract.is_a?(Optional) + contract.within_opt_hash! + !hash.key?(key) || Contract.valid?(hash[key], contract) + end - def valid?(value) - ensure_within_opt_hash - Contract.valid?(value, contract) - end + def initialize(contract) + @contract = contract + @within_opt_hash = false + end - def to_s - "Optional[#{formatted_contract}]" - end + def within_opt_hash! + @within_opt_hash = true + self + end - def inspect - to_s - end + def valid?(value) + ensure_within_opt_hash + Contract.valid?(value, contract) + end - private + def to_s + "Optional[#{formatted_contract}]" + end - attr_reader :contract, :within_opt_hash + def inspect + to_s + end - def ensure_within_opt_hash - return if within_opt_hash - fail ArgumentError, UNABLE_TO_USE_OUTSIDE_OF_OPT_HASH - end + private - def formatted_contract - Formatters::InspectWrapper.create(contract) - end - end + attr_reader :contract, :within_opt_hash + + def ensure_within_opt_hash + return if within_opt_hash + fail ArgumentError, UNABLE_TO_USE_OUTSIDE_OF_OPT_HASH + end - # Takes a Contract. - # The contract passes if the contract passes or the given value is nil. - # Maybe(foo) is equivalent to Or[foo, nil]. - class Maybe < Or - def initialize(*vals) - super(*(vals + [nil])) + def formatted_contract + Formatters::InspectWrapper.create(contract) + end end - def include_proc? - @vals.include? Proc + # Takes a Contract. + # The contract passes if the contract passes or the given value is nil. + # Maybe(foo) is equivalent to Or[foo, nil]. + class Maybe < Or + def initialize(*vals) + super(*(vals + [nil])) + end + + def include_proc? + @vals.include? Proc + end end - end - # Used to define contracts on functions passed in as arguments. - # Example: Func[Num => Num] # the function should take a number and return a number - class Func < CallableClass - attr_reader :contracts - def initialize(*contracts) - @contracts = contracts + # Used to define contracts on functions passed in as arguments. + # Example: Func[Num => Num] # the function should take a number and return a number + class Func < CallableClass + attr_reader :contracts + def initialize(*contracts) + @contracts = contracts + end end end + + # Users can still include `Contracts::Core` & `Contracts::Builtin` + include Builtin end diff --git a/lib/contracts/formatters.rb b/lib/contracts/formatters.rb index d97d78f..db9b107 100644 --- a/lib/contracts/formatters.rb +++ b/lib/contracts/formatters.rb @@ -90,7 +90,7 @@ def full? def plain? # Not a type of contract that can have a custom to_s defined - !@value.is_a?(CallableClass) && @value.class != Class + !@value.is_a?(Builtin::CallableClass) && @value.class != Class end def useful_to_s? @@ -103,7 +103,7 @@ def empty_to_s? end def strip_prefix(val) - val.gsub(/^Contracts::/, "") + val.gsub(/^Contracts::Builtin::/, "") end end diff --git a/spec/builtin_contracts_spec.rb b/spec/builtin_contracts_spec.rb index 2f980e6..7db5bbf 100644 --- a/spec/builtin_contracts_spec.rb +++ b/spec/builtin_contracts_spec.rb @@ -392,7 +392,7 @@ def something(hash) end context "given String => Num" do - it { expect(Contracts::HashOf[String, Contracts::Num].to_s).to eq("Hash") } + it { expect(Contracts::HashOf[String, Contracts::Num].to_s).to eq("Hash") } end end end