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 +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
|