Skip to content

Commit

Permalink
Worked a little bit on Remarkable core documentation.
Browse files Browse the repository at this point in the history
  • Loading branch information
josevalim committed Apr 10, 2009
1 parent 6cc527d commit f42b256
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 30 deletions.
42 changes: 40 additions & 2 deletions README
@@ -1,2 +1,40 @@
Remarkable
==========
= Remarkable 3.0

Remarkable is a framework for rspec matchers that supports macros and I18n. It's
constituted of three pieces:

+ Remarkable: the framework with helpers, DSL, I18n and rspec features;

+ Remarkable ActiveRecord: a collection of matchers for ActiveRecord. It
supports all ActiveRecord validations, associations and some extra matchers.

+ Remarkable Rails: a collection of matchers for ActionController. It also
includes MacroStubs, which is a clean DSL for stubbing your controller methods.

In each folder above, you can find a README more detailed description of each piece.

== Install & Upgrade

Install the gem:

sudo gem install remarkable_rails

This will install remarkable, remarkable_activerecord and remarkable_rails gems.

If you are developing matchers, for example hpricot matchers, you should install
only the remarkable "core" gem:

sudo gem install remarkable

Users who are upgrading to Remarkable 3.0, should not find any problem if their
tests are running without deprecation warnings.

== More information

Google group: http://groups.google.com/group/remarkable-core
Bug tracking: http://carlosbrando.lighthouseapp.com/projects/19775-remarkable/overview

== LICENSE

All projects are under MIT LICENSE.

201 changes: 199 additions & 2 deletions remarkable/README
@@ -1,2 +1,199 @@
Remarkable
==========
= Remarkable

This is the core package of Remarkable. It provides a DSL for creating matchers
with I18n support, decoupling messages from matcher's logic and adding rspec
extra features.

== Macros

Each matcher in Remarkable is also available as a macro. So this matcher:

it { should validate_presence_of(:name) }

Can also be written as:

should_validate_presence_of :name

Remarkable adds the possibility to disable macros. So as you could do:

xit { should validate_presence_of(:name) }

You can also do:

xshould_validate_presence_of :name

And it will show in your specs output:

"Example disabled: require name to be set"

== Pending macros

In Rspec you can mark some examples as pending:

it "should have one manager" do
pending("create managers resource")
end

it "should validate associated manager" do
pending("create managers resource")
end

To allow this to work with macros, we created the pending group:

pending "create managers resource" do
should_have_one :manager
should_validate_associated :manager
end

This outputs the same as above.

== I18n

All matchers come with I18n support. You can find an example locale file under
the locale folder of each project.

To change the locale, you have first to add your locale file:

Remarkable.add_locale 'path/to/my_locale.yml'

And then:

Remarkable.locale = :my_locale

Internationalization is powered by the I18n gem. If you are using it with Rails,
it will use the built in gem, otherwise you will have to install the gem by hand:

gem sources -a http://gems.github.com
sudo gem install svenfuchs-i18n

== Creating you own matcher

Create a new matcher is easy. Let's create validate_inclusion_of matcher for
ActiveRecord as an example. A first matcher version would be:

module Remarkable
module ActiveRecord
module Matchers
class ValidateInclusionOfMatcher < Remarkable::ActiveRecord::Base
arguments :attribute
assertion :is_valid?

optional :in
optional :allow_blank, :allow_nil, :default => true

protected

def is_valid?
@options[:in].each do |value|
@subject.send(:"#{@attribute}=", value)
return false, :value => value unless @subject.valid?
end
true
end
end

def validate_inclusion_of(*args)
ValidateInclusionOfMatcher.new(*args).spec(self)
end
end
end
end

This creates a matcher which requires one attribute and has :in, :allow_blank
and :allow_nil as options. So you can call the matcher in the following way:

should_validate_inclusion_of :size, :in => %w(S M L XL)
should_validate_inclusion_of :size, :in => %w(S M L XL), :allow_blank => true

it { should validate_inclusion_of(:size, :in => %w(S M L XL)).allow_nil(true) }
it { should validate_inclusion_of(:size, :in => %w(S M L XL)).allow_nil }

it { should validate_inclusion_of(:size, :in => %w(S M L XL)) }
it { should validate_inclusion_of(:size, :in => %w(S M L XL), :allow_nil => true) }

The assertions methods (in this case, :is_valid?) makes the matcher pass when
it returns true and fail when returns false.

As you noticed, the matcher doesn't have any message on it. You add them on I18n
file. A file for this example would be:

remarkable:
active_record:
validate_inclusion_of:
description: "validate inclusion of {{attribute}}"
expectations:
is_valid: "to be valid when {{attribute}} is {{value}}"
optionals:
in:
positive: "in {{inspect}}"
allow_nil:
positive: "allowing nil values"
negative: "not allowing nil values"
allow_blank:
positive: "allowing blank values"
negative: "allowing blank values"

The optionals are just added to the description message when they are supplied.
Look some description messages examples:

should_validate_inclusion_of :size, :in => %w(S M L XL)
#=> should validate inclusion of size in ["S", "M", "L", "XL"]

should_validate_inclusion_of :size, :in => %w(S M L XL), :allow_nil => true
#=> should validate inclusion of size in ["S", "M", "L", "XL"] and allowing nil values

should_validate_inclusion_of :size, :in => %w(S M L XL), :allow_nil => false
#=> should validate inclusion of size in ["S", "M", "L", "XL"] and not allowing nil values

Please notice that the arguments are available as interpolation option, as well
as the optionals.

The expectations message are set whenever one of the assertions returns false.
In this case, whenever the assertion fails, we are also returning a hash, with
the value that failed:

return false, :value => value

This will tell remarkable to make value as interpolation option too.

Whenever you create all your matchers, you tell remarkable to add them to the
desired rspec example group:

Remarkable.include_matchers!(Remarkable::ActiveRecord, Spec::Example::ExampleGroup)

== Working with collections

Finally, Remarkable also makes easy to deal with collections. The same matcher
could be easily extended to accept a collection of attributes instead of just one:

should_validate_inclusion_of :first_size, :second_size, :in => %w(S M L XL)

For this we have just those two lines:

arguments :attribute
assertion :is_valid?

For:

arguments :collection => :attributes, :as => :attribute
collection_assertion :is_valid?

This means that the collection will be kept in the @attributes instance variable
and for each value in the collection, it will run the :is_valid? assertion.

Whenever running the assertion, it will also set the @attribute (in singular)
variable. In your I18n files, you just need to change your description:

validate_inclusion_of:
description: "validate inclusion of {{attributes}}"

And this will output:

should_validate_inclusion_of :first_size, :second_size, :in => %w(S M L XL)
#=> should validate inclusion of first size and second size in ["S", "M", "L", "XL"]

== More

This is just an overview of the API. You can add extra options to interpolation
by overwriting the interpolation_options methods, you can add callbacks after
initialize your matcher or before asserting and much more!
2 changes: 0 additions & 2 deletions remarkable/lib/remarkable.rb
Expand Up @@ -11,10 +11,8 @@
require File.join(dir, 'remarkable', 'pending')
require File.join(dir, 'remarkable', 'core_ext', 'array')

# Loads rspec files only if spec is defined
if defined?(Spec)
require File.join(dir, 'remarkable', 'rspec')
end

# Add Remarkable default locale file
Remarkable.add_locale File.join(dir, '..', 'locale', 'en.yml')
2 changes: 1 addition & 1 deletion remarkable/lib/remarkable/base.rb
Expand Up @@ -10,7 +10,7 @@ def spec(binding)

private

# Returns the subject class if it's not one.
# Returns the subject class unless it's a class object.
def subject_class
nil unless @subject
@subject.is_a?(Class) ? @subject : @subject.class
Expand Down
3 changes: 1 addition & 2 deletions remarkable/lib/remarkable/dsl.rb
Expand Up @@ -11,13 +11,12 @@ module DSL
] unless self.const_defined?(:ATTR_READERS)

def self.extended(base)
# Load modules
base.extend Assertions
base.send :include, Callbacks
base.send :include, Matches
base.send :include, Optionals

# Set the default value for matcher_arguments
# Initialize matcher_arguments hash with names as an empty array
base.instance_variable_set('@matcher_arguments', { :names => [] })
end

Expand Down
7 changes: 3 additions & 4 deletions remarkable/lib/remarkable/dsl/assertions.rb
Expand Up @@ -24,7 +24,7 @@ module Assertions
# validate_presence_of is a matcher declared as:
#
# class ValidatePresenceOfMatcher < Remarkable::Base
# arguments :collection => :attributes
# arguments :collection => :attributes, :as => :attribute
# end
#
# In this case, Remarkable provides an API that enables you to easily
Expand Down Expand Up @@ -107,7 +107,7 @@ def initialize(#{args.join(',')})
# For example, validate_presence_of can be written as:
#
# class ValidatePresenceOfMatcher < Remarkable::Base
# arguments :collection => :attributes
# arguments :collection => :attributes, :as => :attribute
# collection_assertions :allow_nil?
#
# protected
Expand Down Expand Up @@ -168,8 +168,7 @@ def assertions(*methods, &block)
end
alias :assertion :assertions

# Class method that accepts a block or a Hash that will overwrite
# instance method default_options.
# Class method that accepts a block or a hash to set matcher's default options.
#
def default_options(hash = {}, &block)
if block_given?
Expand Down
4 changes: 2 additions & 2 deletions remarkable/lib/remarkable/dsl/callbacks.rb
Expand Up @@ -8,7 +8,7 @@ def self.included(base)

module ClassMethods
protected
# Class method that accepts a block which is called after initialization.
# Class method that accepts a block or a symbol which is called after initialization.
#
def after_initialize(symbol=nil, &block)
if block_given?
Expand All @@ -18,7 +18,7 @@ def after_initialize(symbol=nil, &block)
end
end

# Class method that accepts a block which is called before assertion.
# Class method that accepts a block or a symbol which is called before assertion.
#
def before_assert(symbol=nil, &block)
if block_given?
Expand Down
20 changes: 6 additions & 14 deletions remarkable/lib/remarkable/dsl/matches.rb
Expand Up @@ -3,7 +3,7 @@ module DSL
module Matches

# For each instance under the collection declared in <tt>arguments</tt>,
# this method will call each method declared in <tt>assertions</tt>.
# this method will call each method declared in <tt>collection_assertions</tt>.
#
# As an example, let's assume you have the following matcher:
#
Expand Down Expand Up @@ -49,14 +49,8 @@ def default_options
{}
end

# Overwrites default_i18n_options to provide collection interpolation,
# arguments and optionals to interpolation options.
#
# Their are appended in the reverse order above. So if you have an optional
# with the same name as an argument, the argument overwrites the optional.
#
# All values are provided calling inspect, so what you will have in your
# I18n available for interpolation is @options[:allow_nil].inspect.
# Overwrites default_i18n_options to provide arguments and optionals
# to interpolation options.
#
# If you still need to provide more other interpolation options, you can
# do that in two ways:
Expand Down Expand Up @@ -95,13 +89,11 @@ def default_i18n_options
i18n_options.update(super)
end

# Methods that return collection_name and object_name as a Hash for
# interpolation.
# Method responsible to add collection as interpolation.
#
def collection_interpolation
options = {}

# Add collection to options
if collection_name = self.class.matcher_arguments[:collection]
collection_name = collection_name.to_sym
collection = instance_variable_get("@#{collection_name}")
Expand All @@ -115,8 +107,8 @@ def collection_interpolation
options
end

# Helper that send the methods given and create a expectation message if
# any returns false.
# Send the assertion methods given and create a expectation message
# if any of those methods returns false.
#
# Since most assertion methods ends with an question mark and it's not
# readable in yml files, we remove question and exclation marks at the
Expand Down
1 change: 0 additions & 1 deletion remarkable/lib/remarkable/matchers.rb
Expand Up @@ -6,7 +6,6 @@ module Matchers; end

# Helper that includes required Remarkable modules into the given klass.
def self.include_matchers!(base, klass)
# Add Remarkable macros core module
klass.send :extend, Remarkable::Macros

if defined?(base::Matchers)
Expand Down

0 comments on commit f42b256

Please sign in to comment.