ianwhite-pickle 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,14 @@
1
+ == 0.1.2
2
+
3
+ * 2 major enhancements
4
+ * create your pickle steps with script/generate pickle
5
+ * Adapter based architecture, supports Machinist, FactoryGirl, and vanilla ActiveRecord
6
+
7
+ * 1 minor enhancement
8
+ * model_names now defaults to subclasses of AR::Base
9
+ * #original_model => #created_model
10
+
11
+
1
12
  == 0.1.1
2
13
 
3
14
  * 1 major enhancement:
@@ -6,6 +17,7 @@
6
17
  * 1 minor enhancement:
7
18
  * Added intentions for pickle in README.textile
8
19
 
20
+
9
21
  == Prior to gems
10
22
 
11
23
  * Initial release: everything is subject to sweeping change
data/README.textile CHANGED
@@ -1,67 +1,98 @@
1
- h1. Pickle [Experimental: In development]
1
+ h1. Pickle
2
2
 
3
- Stick this in vendor/plugins to have cucumber steps that create your models easily from factory_girl/machinist
3
+ Stick this in vendor/plugins to have cucumber steps that create your models easily from factory_girl/machinist/active_record
4
4
 
5
5
  References to the models are stored, not for the purpose of checking the db (although you could use it for
6
6
  that), but for enabling easy reference to urls, and for building complex givens which require a bunch of
7
7
  models collaborating
8
-
9
- h2. Example
10
-
11
- If you have a user model, which has admin? and moderator? predicate methods
12
- and let's say you have an :admin, :user, and :moderator factory, you can do this in a Scenario:
13
-
14
- And an admin exists
15
- And a moderator exists
16
-
17
- # the following is just to show how refs work, obviously you'd not want to test this
18
- Then the moderator should be a moderator
19
- And the admin should be an admin
20
- And the moderator should not be an admin
21
8
 
22
- h2. Usage
9
+ h2. Get Started
23
10
 
24
11
  script/generate pickle
25
12
 
13
+ Now have a look at features/step_definitions/pickle_steps.rb
14
+
26
15
  h2. API
27
16
 
28
17
  h3. Regexps for us in your own steps
29
18
 
30
- If you want to use pickle expressions in your own steps, make sure you
31
- require pickle in that file
32
-
33
- require 'pickle'
34
-
35
- For matching english versions of model names you get
19
+ For capturing english versions of model names you get
36
20
 
37
- #match_model, #capture_model
21
+ *CaptureModel*
38
22
 
39
- Given /^#{capture_model} exists$/ do |model_name|
23
+ <pre>
24
+ Given /^#{CaptureModel} exists$/ do |model_name|
40
25
  model(model_name).should_not == nil
41
26
  end
42
27
 
43
28
  Then /^I should be at the (.*?) page$/ |page|
44
- if page =~ /#{match_model}'s/
45
- url_for(model(page.sub("'s",''))
46
- else
47
- # ...
48
- end
29
+ if page =~ /#{CaptureModel}'s/
30
+ url_for(model($1))
31
+ else
32
+ # ...
49
33
  end
50
-
51
- For matching a field string, you get
34
+ end
35
+ </pre>
52
36
 
53
- #match_fields, #capture_fields
37
+ For capturing a field string, you get
38
+
39
+ *CaptureFields*
54
40
 
55
- Given /^#{capture_model} exists with #{capture_fields}$/ do |model_name, fields|
41
+ <pre>
42
+ Given /^#{CaptureModel} exists with #{CaptureFields}$/ do |model_name, fields|
56
43
  create_model(model_name, fields)
57
44
  end
58
-
45
+ </pre>
59
46
  Take a look at features/step_definitions/pickle_steps.rb for more examples
60
47
 
61
48
  h3. Creating and tracking models
62
49
 
63
- #create_model(<model_name>[, <field string>])
64
-
65
- #find_model(<model_name>[, <field string>])
66
-
67
- #model(<model_model>) # refers to a model that has already been created/found (ie. referred to in a scenario)
50
+ h4. create_model(_model_name_[, _field_string_])
51
+
52
+ This will create a model using the factory name, and optional model label, with the field_string provided. The created model can be later referred to via its name.
53
+
54
+ For example:
55
+
56
+ <pre>
57
+ create_model 'a user' # => will create a User
58
+ create_model 'the user: "1"' # => will create a User, enabling later reference to it with 'user: "1"'
59
+ create_model 'the user', 'name: "Fred"' # => will create a User with attributes {:name => "Fred"}
60
+ </pre>
61
+
62
+ If you don't use Machinist or FactoryGirl, you can still create models, but you must pass in all the fields required to make them valid.
63
+
64
+ However, if you do use Machinist or FactoryGirl, then just use the factory or blueprint name, and will be super sweet.
65
+
66
+ h4. find_model(_model_name_, _field_string_)
67
+
68
+ This will find a model of the passed class matching the passed field string. The found model can be later referred to by its name.
69
+
70
+ For example:
71
+
72
+ <pre>
73
+ find_model('a user', 'name: "Fred'") # => find a user matching those attributes
74
+ </pre>
75
+
76
+ h4. model(_model_name_)
77
+
78
+ Refers to a model that has already been created/found (ie. referred to in a scenario)
79
+
80
+ For example:
81
+
82
+ <pre>
83
+ create_model('a user')
84
+ model('the user') # => refers to above user
85
+ model('a user') # => refers to above user
86
+
87
+ create_model('a car: "herbie"') # => herbie
88
+ create_model('a car: "batmobile"') # => the batmobile
89
+ model('the first car') # => herbie
90
+ model('the 2nd car') # => batmobile
91
+ model('the car "herbie") # => herbie
92
+ </pre>
93
+
94
+ h4. created_model(_model_name_)
95
+
96
+ *model* always pulls from the db to get a fresh copy. If you need to access the originally created object for some reason (perhaps it has a one-time key on it that is used in a mailer for example), you can retreive it with *created_model*
97
+
98
+ h4. more [TODO]
data/Todo.txt CHANGED
@@ -1,5 +0,0 @@
1
- * make a gem release with version number
2
-
3
- * create a generator for the pickle steps
4
-
5
- * add machinist, and fallback to no factory pattern if none available
data/lib/pickle.rb CHANGED
@@ -1,47 +1,19 @@
1
- # Pickle == cucumber + factory
2
- #
3
- # = Usage
4
- # # in features/steps/_env.rb
5
- # require 'pickle/steps'
6
- #
7
- # If you need to configure pickle, do it like this
8
- #
9
- # Pickle::Config.map 'I|me|my', :to => 'user: "me"'
10
- # require 'pickle/steps'
11
- module Pickle
12
- module Version
13
- Major = 0
14
- Minor = 1
15
- Tiny = 1
16
-
17
- String = [Major, Minor, Tiny].join('.')
18
- end
19
-
20
- module Config
21
- class << self
22
- attr_writer :model_names, :factory_names, :names, :mappings
23
-
24
- def model_names
25
- @model_names ||= Dir["#{RAILS_ROOT}/app/models/**/*.rb"].reject{|f| f =~ /observer.rb$/}.map{|f| f.sub("#{RAILS_ROOT}/app/models/",'').sub(".rb",'')}
26
- end
1
+ require 'active_support'
2
+ require 'pickle/adapter'
3
+ require 'pickle/config'
4
+ require 'pickle/parser'
5
+ require 'pickle/parser/with_session'
6
+ require 'pickle/session'
7
+ require 'pickle/injector'
27
8
 
28
- def factory_names
29
- require 'factory_girl'
30
- @factory_names ||= Factory.factories.keys.map(&:to_s)
31
- end
9
+ # make the parser aware of models in the session (for fields refering to models)
10
+ Pickle::Parser.send :include, Pickle::Parser::WithSession
32
11
 
33
- def names
34
- @names ||= (model_names | factory_names)
35
- end
36
-
37
- def mappings
38
- @mappings ||= []
39
- end
40
-
41
- def map(search, options)
42
- raise ArgumentError, "Usage: map 'search', :to => 'replace'" unless search.is_a?(String) && options[:to].is_a?(String)
43
- self.mappings << [search, options[:to]]
44
- end
45
- end
46
- end
47
- end
12
+ # inject the pickle session into integration session if we have one (TODO: inject into merb etc?)
13
+ if defined?(ActionController::Integration::Session)
14
+ Pickle::Injector.inject Pickle::Session, :into => ActionController::Integration::Session
15
+ end
16
+
17
+ # shortcuts for useful regexps when defining pickle steps
18
+ CaptureModel = Pickle.parser.capture_model
19
+ CaptureFields = Pickle.parser.capture_fields
@@ -0,0 +1,86 @@
1
+ module Pickle
2
+ # Abstract Factory adapter class, if you have a factory type setup, you
3
+ # can easily create an adaptor to make it work with Pickle.
4
+ #
5
+ # The factory adaptor must have a #factories class method that returns
6
+ # its instances, and each instance must respond to a #name method which
7
+ # identifies the factory by name (default is attr_reader for @name), and a
8
+ # #create method which takes an optional attributes hash,
9
+ # and returns a newly created object
10
+ class Adapter
11
+ attr_reader :name
12
+
13
+ def self.factories
14
+ raise NotImplementedError, "return an array of factory adapter objects"
15
+ end
16
+
17
+ def create(attrs = {})
18
+ raise NotImplementedError, "create and return an object with the given attributes"
19
+ end
20
+
21
+ # by default the models are active_record subclasses, but you can set this to whatever classes you want
22
+ class << self
23
+ attr_writer :model_classes
24
+
25
+ def model_classes
26
+ @model_classes ||= returning(::ActiveRecord::Base.send(:subclasses)) do |classes|
27
+ defined?(CGI::Session::ActiveRecordStore::Session) && classes.delete(CGI::Session::ActiveRecordStore::Session)
28
+ end
29
+ end
30
+ end
31
+
32
+ # machinist adapter
33
+ class Machinist < Adapter
34
+ def self.factories
35
+ factories = []
36
+ model_classes.each do |klass|
37
+ factories << new(klass, "make") if klass.instance_variable_get('@blueprint')
38
+ # if there are special make_special methods, add blueprints for them
39
+ klass.methods.select{|m| m =~ /^make_/ && m !~ /_unsaved$/}.each do |method|
40
+ factories << new(klass, method)
41
+ end
42
+ end
43
+ factories
44
+ end
45
+
46
+ def initialize(klass, method)
47
+ @klass, @method = klass, method
48
+ @name = (@method =~ /make_/ ? "#{@method.sub('make_','')}_" : "") + @klass.name.underscore.gsub('/','_')
49
+ end
50
+
51
+ def create(attrs = {})
52
+ @klass.send(@method, attrs)
53
+ end
54
+ end
55
+
56
+ # factory-girl adapter
57
+ class FactoryGirl < Adapter
58
+ def self.factories
59
+ (::Factory.factories.keys rescue []).map {|key| new(key)}
60
+ end
61
+
62
+ def initialize(key)
63
+ @name = key.to_s
64
+ end
65
+
66
+ def create(attrs = {})
67
+ Factory.create(@name, attrs)
68
+ end
69
+ end
70
+
71
+ # fallback active record adapter
72
+ class ActiveRecord < Adapter
73
+ def self.factories
74
+ model_classes.map {|klass| new(klass) }
75
+ end
76
+
77
+ def initialize(klass)
78
+ @klass, @name = klass, klass.name.underscore.gsub('/','_')
79
+ end
80
+
81
+ def create(attrs = {})
82
+ @klass.send(:create!, attrs)
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,51 @@
1
+ require 'ostruct'
2
+
3
+ module Pickle
4
+ def self.config(&block)
5
+ returning(@config ||= Config.new) do |config|
6
+ config.configure(&block) if block_given?
7
+ end
8
+ end
9
+
10
+ class Config
11
+ attr_writer :adapters, :factories, :mappings
12
+
13
+ def initialize(&block)
14
+ configure(&block) if block_given?
15
+ end
16
+
17
+ def configure(&block)
18
+ yield(self)
19
+ end
20
+
21
+ def adapters
22
+ @adapters ||= [:machinist, :factory_girl, :active_record]
23
+ end
24
+
25
+ def adapter_classes
26
+ adapters.map {|a| a.is_a?(Class) ? a : "pickle/adapter/#{a}".classify.constantize}
27
+ end
28
+
29
+ def factories
30
+ @factories ||= adapter_classes.reverse.inject({}) do |factories, adapter|
31
+ factories.merge(adapter.factories.inject({}){|h, f| h.merge(f.name => f)})
32
+ end
33
+ end
34
+
35
+ def factory_names
36
+ factories.keys
37
+ end
38
+
39
+ def mappings
40
+ @mappings ||= []
41
+ end
42
+
43
+ # Usage: map 'me', 'myself', 'I', :to => 'user: "me"'
44
+ def map(*args)
45
+ options = args.extract_options!
46
+ raise ArgumentError, "Usage: map 'search' [, 'search2', ...] :to => 'replace'" unless args.any? && options[:to].is_a?(String)
47
+ search = args.size == 1 ? args.first.to_s : "(?:#{args.join('|')})"
48
+ self.mappings << OpenStruct.new(:search => search, :replace => options[:to])
49
+ end
50
+ end
51
+ end
@@ -1,18 +1,18 @@
1
1
  module Pickle
2
2
  module Injector
3
- def self.inject(session, options = {})
3
+ def self.inject(session_class, options = {})
4
4
  target = options[:into] || ActionController::Integration::Session
5
- session_name = session.name.underscore.gsub('/','_')
5
+ session_method = options[:name] || session_class.name.underscore.gsub('/','_')
6
+ session_options = options[:options] || {}
6
7
 
7
- target.class_eval <<-end_eval, __FILE__, __LINE__
8
- def #{session_name}
9
- @#{session_name} ||= #{session.name}.new
10
- end
11
- end_eval
12
-
13
- delegate_methods = session.instance_methods - Object.instance_methods
14
- delegate_methods << {:to => session_name}
15
- target.delegate *delegate_methods
8
+ # create a session object on demand (in target)
9
+ target.send(:define_method, session_method) do
10
+ instance_variable_get("@#{session_method}") || instance_variable_set("@#{session_method}", session_class.new(session_options))
11
+ end
12
+
13
+ # delegate session methods to the session object (in target)
14
+ delegate_methods = session_class.public_instance_methods - Object.instance_methods
15
+ target.delegate *(delegate_methods + [{:to => session_method}])
16
16
  end
17
17
  end
18
18
  end
data/lib/pickle/parser.rb CHANGED
@@ -1,49 +1,38 @@
1
+ require 'pickle/parser/matchers'
2
+
1
3
  module Pickle
2
- module Parser
3
- module Match
4
- Ordinal = '(?:\d+(?:st|nd|rd|th))'
5
- Index = "(?:first|last|#{Ordinal})"
6
- Prefix = '(?:1 |a |an |another |the |that )'
7
- Quoted = '(?:[^\\"]|\\.)*'
8
- Name = '(?::? "' + Quoted + '")'
9
-
10
- # model matching - depends on Parse::Config, so load before this if non standard config requried
11
- ModelMapping = "(?:#{Pickle::Config.mappings.map(&:first).map{|s| "(?:#{s})"}.join('|')})"
12
- ModelName = "(?:#{Pickle::Config.names.map{|n| n.to_s.gsub('_','[_ ]').gsub('/','[:\/ ]')}.join('|')})"
13
- IndexedModel = "(?:(?:#{Index} )?#{ModelName})"
14
- NamedModel = "(?:#{ModelName}#{Name}?)"
15
- Model = "(?:#{ModelMapping}|#{Prefix}?(?:#{IndexedModel}|#{NamedModel}))"
16
- Field = '(?:\w+: (?:' + Model + '|"' + Quoted + '"))'
17
- Fields = "(?:#{Field}, )*#{Field}"
18
-
19
- end
4
+ def self.parser(options = {})
5
+ @parser ||= Parser.new(options)
6
+ end
7
+
8
+ class Parser
9
+ include Matchers
20
10
 
21
- # these are expressions which capture a sub expression
22
- module Capture
23
- Ordinal = '(?:(\d+)(?:st|nd|rd|th))'
24
- Name = '(?::? "(' + Match::Quoted + ')")'
25
- Field = '(?:(\w+): "(' + Match::Quoted + ')")'
11
+ attr_reader :config
12
+
13
+ def initialize(options = {})
14
+ @config = options[:config] || Pickle.config
26
15
  end
27
16
 
28
17
  # given a string like 'foo: "bar", bar: "baz"' returns {"foo" => "bar", "bar" => "baz"}
29
18
  def parse_fields(fields)
30
19
  if fields.blank?
31
20
  {}
32
- elsif fields =~ /^#{Match::Fields}$/
33
- fields.scan(/(#{Match::Field})(?:,|$)/).inject({}) do |m, match|
21
+ elsif fields =~ /^#{match_fields}$/
22
+ fields.scan(/(#{match_field})(?:,|$)/).inject({}) do |m, match|
34
23
  m.merge(parse_field(match[0]))
35
24
  end
36
25
  else
37
- raise ArgumentError, "The fields string is not in the correct format.\n\n'#{fields}' did not match: #{Match::Fields}"
26
+ raise ArgumentError, "The fields string is not in the correct format.\n\n'#{fields}' did not match: #{match_fields}"
38
27
  end
39
28
  end
40
29
 
41
30
  # given a string like 'foo: "bar"' returns {key => value}
42
31
  def parse_field(field)
43
- if field =~ /^#{Capture::Field}$/
32
+ if field =~ /^#{capture_key_and_value_in_field}$/
44
33
  { $1 => $2 }
45
34
  else
46
- raise ArgumentError, "The field argument is not in the correct format.\n\n'#{field}' did not match: #{Match::Field}"
35
+ raise ArgumentError, "The field argument is not in the correct format.\n\n'#{field}' did not match: #{match_field}"
47
36
  end
48
37
  end
49
38
 
@@ -52,30 +41,28 @@ module Pickle
52
41
  str.to_s.gsub(' ','_').underscore
53
42
  end
54
43
 
44
+ # return [factory_name, name or integer index]
45
+ def parse_model(model_name)
46
+ apply_mappings!(model_name)
47
+ if /#{capture_index} #{capture_factory}$/ =~ model_name
48
+ [canonical($2), parse_index($1)]
49
+ elsif /#{capture_factory}#{capture_name_in_label}?$/ =~ model_name
50
+ [canonical($1), canonical($2)]
51
+ end
52
+ end
53
+
55
54
  def parse_index(index)
56
55
  case index
57
56
  when '', /last/ then -1
58
- when /#{Capture::Ordinal}/ then $1.to_i - 1
57
+ when /#{capture_number_in_ordinal}/ then $1.to_i - 1
59
58
  when /first/ then 0
60
59
  end
61
60
  end
62
-
61
+
63
62
  private
64
- # return [factory, name or integer index]
65
- def parse_model(name)
66
- apply_mappings!(name)
67
- if /(#{Match::ModelName})#{Capture::Name}$/ =~ name
68
- [canonical($1), canonical($2)]
69
- elsif /(#{Match::Index}) (#{Match::ModelName})$/ =~ name
70
- [canonical($2), parse_index($1)]
71
- else
72
- /(#{Match::ModelName})#{Capture::Name}?$/ =~ name && [canonical($1), canonical($2)]
73
- end
74
- end
75
-
76
63
  def apply_mappings!(string)
77
- Pickle::Config.mappings.each do |(search, replace)|
78
- string.sub! /^(?:#{search})$/, replace
64
+ config.mappings.each do |mapping|
65
+ string.sub! /^#{mapping.search}$/, mapping.replace
79
66
  end
80
67
  end
81
68
  end