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 +12 -0
- data/README.textile +70 -39
- data/Todo.txt +0 -5
- data/lib/pickle.rb +17 -45
- data/lib/pickle/adapter.rb +86 -0
- data/lib/pickle/config.rb +51 -0
- data/lib/pickle/injector.rb +11 -11
- data/lib/pickle/parser.rb +31 -44
- data/lib/pickle/parser/matchers.rb +75 -0
- data/lib/pickle/parser/with_session.rb +24 -0
- data/lib/pickle/session.rb +26 -25
- data/lib/pickle/version.rb +9 -0
- data/rails_generators/pickle/pickle_generator.rb +19 -0
- data/rails_generators/pickle/templates/env.rb +9 -0
- data/rails_generators/pickle/templates/pickle_steps.rb +23 -0
- data/spec/lib/pickle_adapter_spec.rb +128 -0
- data/spec/lib/pickle_config_spec.rb +82 -28
- data/spec/lib/pickle_injector_spec.rb +3 -3
- data/spec/lib/pickle_parser_matchers_spec.rb +54 -0
- data/spec/lib/pickle_parser_spec.rb +56 -77
- data/spec/lib/pickle_session_spec.rb +38 -36
- metadata +14 -3
- data/lib/pickle/steps.rb +0 -27
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
|
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.
|
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
|
-
|
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
|
-
|
21
|
+
*CaptureModel*
|
38
22
|
|
39
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
end
|
29
|
+
if page =~ /#{CaptureModel}'s/
|
30
|
+
url_for(model($1))
|
31
|
+
else
|
32
|
+
# ...
|
49
33
|
end
|
50
|
-
|
51
|
-
|
34
|
+
end
|
35
|
+
</pre>
|
52
36
|
|
53
|
-
|
37
|
+
For capturing a field string, you get
|
38
|
+
|
39
|
+
*CaptureFields*
|
54
40
|
|
55
|
-
|
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
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
data/lib/pickle.rb
CHANGED
@@ -1,47 +1,19 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
29
|
-
|
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
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
data/lib/pickle/injector.rb
CHANGED
@@ -1,18 +1,18 @@
|
|
1
1
|
module Pickle
|
2
2
|
module Injector
|
3
|
-
def self.inject(
|
3
|
+
def self.inject(session_class, options = {})
|
4
4
|
target = options[:into] || ActionController::Integration::Session
|
5
|
-
|
5
|
+
session_method = options[:name] || session_class.name.underscore.gsub('/','_')
|
6
|
+
session_options = options[:options] || {}
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
delegate_methods =
|
14
|
-
delegate_methods
|
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
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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 =~ /^#{
|
33
|
-
fields.scan(/(#{
|
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: #{
|
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 =~ /^#{
|
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: #{
|
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 /#{
|
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
|
-
|
78
|
-
string.sub!
|
64
|
+
config.mappings.each do |mapping|
|
65
|
+
string.sub! /^#{mapping.search}$/, mapping.replace
|
79
66
|
end
|
80
67
|
end
|
81
68
|
end
|