ianwhite-pickle 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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