scheherazade 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.md +140 -0
- data/Rakefile +38 -0
- data/lib/scheherazade/bare.rb +3 -0
- data/lib/scheherazade/character_builder.rb +166 -0
- data/lib/scheherazade/extension.rb +7 -0
- data/lib/scheherazade/log.rb +33 -0
- data/lib/scheherazade/story.rb +169 -0
- data/lib/scheherazade/version.rb +3 -0
- data/lib/scheherazade.rb +12 -0
- metadata +110 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2012 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
# Scheherazade
|
2
|
+
|
3
|
+
With Sheherazade's imagination and storytelling skills, fixtures can be as entertaining as the "Arabian Nights".
|
4
|
+
|
5
|
+
## Goals
|
6
|
+
|
7
|
+
Scheherazade
|
8
|
+
|
9
|
+
* imagines plausible characters (creates valid objects automatically)
|
10
|
+
* keeps track of her story (reuse objects within a given context)
|
11
|
+
* isn't wearing much (minimal DSL, no instance_eval)
|
12
|
+
|
13
|
+
## Simple Example
|
14
|
+
|
15
|
+
# Say we have a model like:
|
16
|
+
class Department
|
17
|
+
belongs_to :company
|
18
|
+
has_many :employees
|
19
|
+
validates_presence_of :company, :name
|
20
|
+
end
|
21
|
+
|
22
|
+
# Without any configuration, if we write in a test:
|
23
|
+
story.imagine(Department) # creates a Department,
|
24
|
+
# with a default name,
|
25
|
+
# associated to a new Company
|
26
|
+
story.imagine(Department) # creates another Department
|
27
|
+
# with another name
|
28
|
+
# and associated to the same Company
|
29
|
+
|
30
|
+
## Features
|
31
|
+
|
32
|
+
For FactoryGirl (or Machinist) users: a Factory (or Blueprint) corresponds loosely to a Character
|
33
|
+
|
34
|
+
### Characters (Models)
|
35
|
+
|
36
|
+
Scheherazade creates ActiveRecord objects. The types of objects are called a 'character'. A generic character could be a `User` (or equivalently `:user`) or there could be more specialized characters (say `:admin`).
|
37
|
+
|
38
|
+
All your models have a default character type; you can use the class directly or the corresponding symbol. Specialized characters must be defined within the current story before they can be used.
|
39
|
+
|
40
|
+
### Context (Story)
|
41
|
+
|
42
|
+
The current story holds:
|
43
|
+
|
44
|
+
* information on how to build characters
|
45
|
+
* the last built character (called the current character)
|
46
|
+
* any options you want
|
47
|
+
|
48
|
+
Scheherazade can tell nested stories and all this information is inherited.
|
49
|
+
|
50
|
+
### Automatic objects (Characters)
|
51
|
+
|
52
|
+
Scheherazade can be setup to generate attributes for any model. She will also by default generate values for attributes that are needed.
|
53
|
+
|
54
|
+
If the object imagined is not valid, she will try to makeup values for the attributes that have errors (e.g. because of a `validates_presence_of` with a condition that is `true`)
|
55
|
+
|
56
|
+
By default, assocations will reuse objects within the current story. For example, two imagined comments will automatically be about the current blog and posted by the current user.
|
57
|
+
|
58
|
+
### Logging
|
59
|
+
|
60
|
+
Scheherazade is meant to testing and has a logging feature that makes it easier to know what's going on. Turn it on with `Scheherazade.logger.on`
|
61
|
+
|
62
|
+
## Documentation
|
63
|
+
|
64
|
+
The main class is `Story` and there are 2 important methods: Story#imagine and Story#fill. Story#get is a simple shorthand to get the current character or create it if there isn't any.
|
65
|
+
|
66
|
+
## Complete example
|
67
|
+
|
68
|
+
class User
|
69
|
+
belongs_to :blog
|
70
|
+
validates_presence_of :first_name, :last_name
|
71
|
+
end
|
72
|
+
|
73
|
+
class Blog
|
74
|
+
has_many :users
|
75
|
+
has_one :admin, :class_name => "User", :condition => {:admin => true}
|
76
|
+
validates_presence_of :admin
|
77
|
+
|
78
|
+
has_many :posts
|
79
|
+
end
|
80
|
+
|
81
|
+
story.instance_eval do
|
82
|
+
fill User, :email do
|
83
|
+
fill :admin, :admin => true,
|
84
|
+
:nickname => ->(user, sequence){"The boss #{sequence}"}
|
85
|
+
end
|
86
|
+
|
87
|
+
fill Blog, :posts
|
88
|
+
end
|
89
|
+
|
90
|
+
## To do
|
91
|
+
|
92
|
+
Configurable automatic attributes
|
93
|
+
Finish doc
|
94
|
+
Finish specs
|
95
|
+
|
96
|
+
## Why? Scheherazade vs FactoryGirl vs Machinist
|
97
|
+
|
98
|
+
FactoryGirl and Machinist are DSLs to create ActiveRecord objects.
|
99
|
+
|
100
|
+
Both make it tedious to deal with nested structures. Example:
|
101
|
+
|
102
|
+
# / Location - Building - Product
|
103
|
+
# Company {
|
104
|
+
# \ Department — Employee
|
105
|
+
|
106
|
+
company = FactoryGirl.create(:company)
|
107
|
+
location = FactoryGirl.create(:location, :company => company)
|
108
|
+
building = FactoryGirl.create(:building, :location => location)
|
109
|
+
product = FactoryGirl.create(:product, :building => building)
|
110
|
+
department = FactoryGirl.create(:department, :company => company)
|
111
|
+
employee = FactoryGirl.create(:employee, :department => department)
|
112
|
+
|
113
|
+
Since Scheherazade reuses the characters she invents, the above example can be written:
|
114
|
+
|
115
|
+
product = story.imagine Product
|
116
|
+
employee = story.imagine Employee
|
117
|
+
|
118
|
+
Moreover, the above two lines can often be skipped altogether and by replacing the few instances of `product` by `story.get(Product)` which will return the current Product (and imagine one when called for the first time).
|
119
|
+
|
120
|
+
FactoryGirl is also terrible for dealing with `has_many` associations. Scheherazade is clever about these.
|
121
|
+
|
122
|
+
FactoryGirl needs all factories to be created explicitly, and all attributes to be generated must also be explictly defined. Scheherazade uses convention over configuration.
|
123
|
+
|
124
|
+
## Installation
|
125
|
+
|
126
|
+
Add this line to your application's Gemfile:
|
127
|
+
|
128
|
+
gem 'scheherazade'
|
129
|
+
|
130
|
+
And then execute:
|
131
|
+
|
132
|
+
$ bundle
|
133
|
+
|
134
|
+
## Contributing
|
135
|
+
|
136
|
+
1. Fork it
|
137
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
138
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
139
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
140
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'rdoc/task'
|
9
|
+
rescue LoadError
|
10
|
+
require 'rdoc/rdoc'
|
11
|
+
require 'rake/rdoctask'
|
12
|
+
RDoc::Task = Rake::RDocTask
|
13
|
+
end
|
14
|
+
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
+
rdoc.rdoc_dir = 'rdoc'
|
17
|
+
rdoc.title = 'Scheherazade'
|
18
|
+
rdoc.options << '--line-numbers'
|
19
|
+
rdoc.rdoc_files.include('README.rdoc')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
|
26
|
+
Bundler::GemHelper.install_tasks
|
27
|
+
|
28
|
+
require 'rake/testtask'
|
29
|
+
|
30
|
+
Rake::TestTask.new(:test) do |t|
|
31
|
+
t.libs << 'lib'
|
32
|
+
t.libs << 'test'
|
33
|
+
t.pattern = 'test/**/*_test.rb'
|
34
|
+
t.verbose = false
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
task :default => :test
|
@@ -0,0 +1,166 @@
|
|
1
|
+
module Scheherazade
|
2
|
+
class CharacterBuilder < Struct.new(:character)
|
3
|
+
AUTO = 'Auto' # Note: must be equal? to AUTO (i.e. same object), not just ==
|
4
|
+
|
5
|
+
def initialize(character)
|
6
|
+
super
|
7
|
+
@chain = [character]
|
8
|
+
while (c = story.characters[@chain.first])
|
9
|
+
@chain.unshift c
|
10
|
+
end
|
11
|
+
@model = @chain.first
|
12
|
+
story.send(:building) << @ar = @model.new
|
13
|
+
@chain.each{|c| story.current[c] = @ar }
|
14
|
+
end
|
15
|
+
|
16
|
+
# builds a character, filling required attributes,
|
17
|
+
# default attributes for this type of character and
|
18
|
+
# the attributes given.
|
19
|
+
#
|
20
|
+
# If the built object is invalid, the attributes with
|
21
|
+
# errors will also be filled.
|
22
|
+
#
|
23
|
+
# An invalid object may be returned. In particular, it is
|
24
|
+
# possible that it is only invalid because an associated
|
25
|
+
# model is not yet valid.
|
26
|
+
#
|
27
|
+
def build(attribute_list = nil)
|
28
|
+
@seq = (story.counter[@model] += 1)
|
29
|
+
lists = [required_attributes, *default_attribute_lists, attribute_list]
|
30
|
+
attribute_list = lists.map{|al| canonical(al)}.inject(:merge)
|
31
|
+
log(:building, attribute_list)
|
32
|
+
set_attributes(attribute_list)
|
33
|
+
unless @ar.valid?
|
34
|
+
attribute_list = canonical(@ar.errors.map{|attr, _| attr})
|
35
|
+
log(:fixing_errors, attribute_list)
|
36
|
+
set_attributes(attribute_list)
|
37
|
+
end
|
38
|
+
yield @ar if block_given?
|
39
|
+
log(:final_value, @ar)
|
40
|
+
@ar
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
def log(action, *args)
|
45
|
+
Scheherazade.log(action, @model, *args)
|
46
|
+
end
|
47
|
+
|
48
|
+
def required_attributes
|
49
|
+
@model.validators.select{|v| v.is_a?(ActiveModel::Validations::PresenceValidator) && v.options.empty?}
|
50
|
+
.flat_map(&:attributes)
|
51
|
+
end
|
52
|
+
|
53
|
+
def default_attribute_lists
|
54
|
+
story.fill_attributes.values_at(*@chain)
|
55
|
+
end
|
56
|
+
|
57
|
+
def set_attributes(attributes)
|
58
|
+
attributes.each do |attr, value|
|
59
|
+
set_attribute(attr, value)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def set_attribute(attribute, value = AUTO)
|
64
|
+
if assoc = @model.reflect_on_association(attribute)
|
65
|
+
# It's possible that build records are not yet valid, so we
|
66
|
+
# can expect errors on associations. At least for these reasons:
|
67
|
+
return if @ar.send(attribute).present?
|
68
|
+
|
69
|
+
value = assoc.klass.name.underscore.to_sym if value == AUTO
|
70
|
+
value = value_for_association(assoc, value)
|
71
|
+
elsif value.equal?(AUTO)
|
72
|
+
value = automatic_value_for_basic_attribute(attribute)
|
73
|
+
end
|
74
|
+
log :setting, attribute, value
|
75
|
+
@ar.send("#{attribute}=", value)
|
76
|
+
end
|
77
|
+
|
78
|
+
def value_for_association(assoc, associated_character)
|
79
|
+
log(:setting_assocation, associated_character)
|
80
|
+
opts = {assoc.active_record.name.underscore.to_sym => @ar} if [:has_many, :has_one].include? assoc.macro
|
81
|
+
ar = if associated_character.is_a?(Symbol)
|
82
|
+
story.current[associated_character] || self.class.new(associated_character).build(opts)
|
83
|
+
else
|
84
|
+
associated_character
|
85
|
+
end
|
86
|
+
case assoc.macro
|
87
|
+
when :belongs_to
|
88
|
+
ar
|
89
|
+
when :has_and_belongs_to_many
|
90
|
+
[ar]
|
91
|
+
when :has_one
|
92
|
+
ar
|
93
|
+
when :has_many
|
94
|
+
if ar && ar.persisted?
|
95
|
+
if ar.send(assoc.active_record.name.underscore) != @ar
|
96
|
+
log :additional_character, assoc.name
|
97
|
+
[self.class.new(associated_character).build(opts)]
|
98
|
+
else
|
99
|
+
[ar]
|
100
|
+
end
|
101
|
+
else
|
102
|
+
[*ar]
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def automatic_value_for_basic_attribute(attribute)
|
108
|
+
seq_string = " {#{@seq}}" if @seq > 1
|
109
|
+
case @model.columns_hash[attribute.to_s].try(:type)
|
110
|
+
when :integer then @seq
|
111
|
+
when :float then @seq
|
112
|
+
when :decimal then "#{@seq}.99"
|
113
|
+
when :datetime then Time.now
|
114
|
+
when :date then Date.today
|
115
|
+
when :string then
|
116
|
+
case attribute
|
117
|
+
when :email
|
118
|
+
"joe#{@seq}@example.com"
|
119
|
+
when :name, :title
|
120
|
+
"Example #{@model.name.humanize}#{seq_string}"
|
121
|
+
else "Some #{attribute}#{seq_string}"
|
122
|
+
end
|
123
|
+
when :text then "Some #{attribute} text#{seq_string}"
|
124
|
+
when :boolean then false
|
125
|
+
else
|
126
|
+
if enum = @model.enum_definitions[attribute]
|
127
|
+
@model.send(attribute.to_s.pluralize).keys.first
|
128
|
+
else
|
129
|
+
puts "Unknown type for #{@model}##{attribute}"
|
130
|
+
nil
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Converts an attribute_list to a single Hash;
|
136
|
+
# some of the values may be set to AUTO.
|
137
|
+
#
|
138
|
+
# canonical [:foo, {:bar => 42}]
|
139
|
+
# # => {:foo => AUTO, :bar => 42}
|
140
|
+
#
|
141
|
+
def canonical(attribute_list)
|
142
|
+
case attribute_list
|
143
|
+
when nil then {}
|
144
|
+
when Hash then attribute_list
|
145
|
+
when Array
|
146
|
+
attribute_list.map! do |attributes|
|
147
|
+
case attributes
|
148
|
+
when Symbol
|
149
|
+
{attributes => AUTO}
|
150
|
+
when Hash
|
151
|
+
attributes
|
152
|
+
else
|
153
|
+
raise "Unexpected attributes #{attributes}"
|
154
|
+
end
|
155
|
+
end
|
156
|
+
attribute_list.inject({}, :merge)
|
157
|
+
else
|
158
|
+
raise "Unexpected attribute_list #{attribute_list}"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def story
|
163
|
+
Story.current
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Scheherazade
|
2
|
+
class Logger
|
3
|
+
EVENTS = [:saving, :building, :fixing_errors, :final_value, :setting_assocation, :setting, :additional_character].to_set.freeze
|
4
|
+
|
5
|
+
def off
|
6
|
+
@events = []
|
7
|
+
end
|
8
|
+
|
9
|
+
def on
|
10
|
+
only
|
11
|
+
end
|
12
|
+
|
13
|
+
def only *events_and_characters
|
14
|
+
events_and_characters = events_and_characters.to_set
|
15
|
+
@events = (EVENTS & events_and_characters).presence || EVENTS
|
16
|
+
@characters = (events_and_characters - @events).to_set.presence
|
17
|
+
end
|
18
|
+
|
19
|
+
def log(event, character, *rest)
|
20
|
+
if @events && @events.include?(event) && (@characters.nil? || @characters.include?(character))
|
21
|
+
puts "#{character}: #{rest.unshift(event).join(' | ')}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.logger
|
27
|
+
Thread.current[:scheherazade_logger] ||= Logger.new
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.log(*args)
|
31
|
+
logger.log(*args)
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
module Scheherazade
|
2
|
+
class Story < Hash
|
3
|
+
attr_reader :fill_attributes, :characters, :counter, :current
|
4
|
+
|
5
|
+
def self.current
|
6
|
+
Thread.current[:scheherazade_stories].last
|
7
|
+
end
|
8
|
+
|
9
|
+
# Begins a story within the current story.
|
10
|
+
# Should be balanced with a call to +end+
|
11
|
+
#
|
12
|
+
def self.begin
|
13
|
+
(Thread.current[:scheherazade_stories] ||= []).push Story.new
|
14
|
+
current
|
15
|
+
end
|
16
|
+
|
17
|
+
# Ends the current substory and comes back
|
18
|
+
# to the previous current story
|
19
|
+
#
|
20
|
+
def self.end(rollback = false)
|
21
|
+
current.send :rollback if rollback
|
22
|
+
Thread.current[:scheherazade_stories].pop
|
23
|
+
current
|
24
|
+
end
|
25
|
+
|
26
|
+
# Begins a substory, yields, and ends the story
|
27
|
+
#
|
28
|
+
def self.tell(opts = nil)
|
29
|
+
yield self.begin
|
30
|
+
ensure
|
31
|
+
self.end(opts && opts[:rollback])
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize(parent = self.class.current)
|
35
|
+
super(){|h, k| parent[k] if parent }
|
36
|
+
@parent = parent
|
37
|
+
@current = Hash.new{|h, k| parent.current[k] if parent}
|
38
|
+
@fill_attributes = Hash.new{|h, k| parent.fill_attributes[k] if parent }
|
39
|
+
@characters = Hash.new do |h, k|
|
40
|
+
if parent
|
41
|
+
parent.characters[k]
|
42
|
+
elsif k.is_a?(Symbol)
|
43
|
+
k.to_s.camelize.constantize
|
44
|
+
end
|
45
|
+
end
|
46
|
+
@counter = parent ? parent.counter.dup : Hash.new(0)
|
47
|
+
@filling = []
|
48
|
+
@after_imagine = {}
|
49
|
+
@built = []
|
50
|
+
end
|
51
|
+
|
52
|
+
# Creates a character with the given attributes
|
53
|
+
#
|
54
|
+
# A character can be designated either by the model (e.g. `User`), the corresponding
|
55
|
+
# symbol (`:user`) or the symbol for a specialized type of character, defined using +fill+
|
56
|
+
# (e.g. `:admin`).
|
57
|
+
#
|
58
|
+
# The attributes can be nil, a list of symbols, a hash or a combination of both
|
59
|
+
# These, along with attributes passed to +fill+ for the current stories
|
60
|
+
# and the mandatory attributes for the model will be provided.
|
61
|
+
#
|
62
|
+
# If some fields generate validation errors, they will be provided also.
|
63
|
+
#
|
64
|
+
# For associations, the values can also be a character (Symbol or Model),
|
65
|
+
# integers (meaning the default Model * that integer) or arrays of characters.
|
66
|
+
#
|
67
|
+
# imagine(:user, :account, :company => :fortune_500_company, :emails => 3)
|
68
|
+
#
|
69
|
+
# Similarly:
|
70
|
+
#
|
71
|
+
# User.imagine(...)
|
72
|
+
# :user.imagine(...)
|
73
|
+
#
|
74
|
+
# This record (and any other imagined through associations) will become the
|
75
|
+
# current character in the current story:
|
76
|
+
#
|
77
|
+
# story.current[User] # => nil
|
78
|
+
# story.tell do
|
79
|
+
# :admin.imagine # => A User record
|
80
|
+
# story.current[:admin] # => same
|
81
|
+
# story.current[User] # => same
|
82
|
+
# end
|
83
|
+
# story.current[User] # => nil
|
84
|
+
#
|
85
|
+
def imagine(character, attributes = nil)
|
86
|
+
prev, @building = @building, [] # because method might be re-entrant
|
87
|
+
CharacterBuilder.new(character).build(attributes) do |ar|
|
88
|
+
ar.save!
|
89
|
+
# While errors on records associated with :has_many will prevent records
|
90
|
+
# from being saved, they won't for :belongs_to, so:
|
91
|
+
@building.each do |ar|
|
92
|
+
ar.valid? and raise ActiveRecord::RecordInvalid, ar.errors unless ar.persisted?
|
93
|
+
end
|
94
|
+
Scheherazade.log(:saving, character, ar)
|
95
|
+
handle_callbacks(@building)
|
96
|
+
end
|
97
|
+
ensure
|
98
|
+
@built.concat(@building)
|
99
|
+
@building = prev
|
100
|
+
end
|
101
|
+
|
102
|
+
def get(character)
|
103
|
+
current[character] || imagine(character)
|
104
|
+
end
|
105
|
+
|
106
|
+
def begin
|
107
|
+
self.class.begin
|
108
|
+
end
|
109
|
+
|
110
|
+
def end
|
111
|
+
self.class.end
|
112
|
+
end
|
113
|
+
|
114
|
+
def tell(opts = nil)
|
115
|
+
self.class.tell(opts){|story| yield story }
|
116
|
+
end
|
117
|
+
|
118
|
+
# Allows one to temporarily override the current characters while
|
119
|
+
# the given block executes
|
120
|
+
#
|
121
|
+
def with(temp_current, &block)
|
122
|
+
old = current.slice(*temp_current.keys)
|
123
|
+
current.merge!(temp_current)
|
124
|
+
instance_eval(&block)
|
125
|
+
ensure
|
126
|
+
current.merge!(old)
|
127
|
+
end
|
128
|
+
|
129
|
+
def fill(character_or_model, *with)
|
130
|
+
fill_attributes[character_or_model] = with
|
131
|
+
@characters[character_or_model] = current_fill if character_or_model.is_a? Symbol
|
132
|
+
begin
|
133
|
+
@filling.push(character_or_model)
|
134
|
+
yield
|
135
|
+
ensure
|
136
|
+
@filling.pop
|
137
|
+
end if block_given?
|
138
|
+
end
|
139
|
+
|
140
|
+
def after_imagine(&block)
|
141
|
+
raise NotImplementedError if current_fill.is_a?(Symbol)
|
142
|
+
@after_imagine[current_fill] = block
|
143
|
+
end
|
144
|
+
|
145
|
+
protected
|
146
|
+
def current_fill
|
147
|
+
@filling.last or raise "Expected to be inside a story.fill"
|
148
|
+
end
|
149
|
+
|
150
|
+
def handle_callbacks(on)
|
151
|
+
@parent.handle_callbacks(on) if @parent
|
152
|
+
on.each do |char|
|
153
|
+
if (ai = @after_imagine[char.class])
|
154
|
+
ai.yield(char)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def rollback
|
160
|
+
@built.each(&:destroy)
|
161
|
+
end
|
162
|
+
|
163
|
+
attr_reader :building
|
164
|
+
private :building
|
165
|
+
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
Scheherazade::Story.begin
|
data/lib/scheherazade.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require_relative "scheherazade/bare"
|
2
|
+
|
3
|
+
# Scheherazade publishes a method imagine for characters:
|
4
|
+
ActiveRecord::Base.extend Scheherazade::Extension # models
|
5
|
+
Symbol.send :include, Scheherazade::Extension # and symbols
|
6
|
+
|
7
|
+
# as well as a global shortcut to get the current story.
|
8
|
+
def story
|
9
|
+
Scheherazade::Story.current
|
10
|
+
end
|
11
|
+
|
12
|
+
# Require directly "scheherazade/bare" if you don't want these.
|
metadata
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: scheherazade
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Marc-André Lafortune
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-07-04 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rails
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '3.0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '3.0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: sqlite3
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec-rails
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
description: With Sheherazade's imagination and storytelling skills, fixtures can
|
63
|
+
be as entertaining as the “Arabian Nights”.
|
64
|
+
email:
|
65
|
+
- github@marc-andre.ca
|
66
|
+
executables: []
|
67
|
+
extensions: []
|
68
|
+
extra_rdoc_files: []
|
69
|
+
files:
|
70
|
+
- lib/scheherazade/bare.rb
|
71
|
+
- lib/scheherazade/character_builder.rb
|
72
|
+
- lib/scheherazade/extension.rb
|
73
|
+
- lib/scheherazade/log.rb
|
74
|
+
- lib/scheherazade/story.rb
|
75
|
+
- lib/scheherazade/version.rb
|
76
|
+
- lib/scheherazade.rb
|
77
|
+
- MIT-LICENSE
|
78
|
+
- Rakefile
|
79
|
+
- README.md
|
80
|
+
homepage: http://github.com/marcandre/scheherazade
|
81
|
+
licenses: []
|
82
|
+
post_install_message:
|
83
|
+
rdoc_options: []
|
84
|
+
require_paths:
|
85
|
+
- lib
|
86
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
87
|
+
none: false
|
88
|
+
requirements:
|
89
|
+
- - ! '>='
|
90
|
+
- !ruby/object:Gem::Version
|
91
|
+
version: '0'
|
92
|
+
segments:
|
93
|
+
- 0
|
94
|
+
hash: -3557737727214523228
|
95
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
96
|
+
none: false
|
97
|
+
requirements:
|
98
|
+
- - ! '>='
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
segments:
|
102
|
+
- 0
|
103
|
+
hash: -3557737727214523228
|
104
|
+
requirements: []
|
105
|
+
rubyforge_project:
|
106
|
+
rubygems_version: 1.8.24
|
107
|
+
signing_key:
|
108
|
+
specification_version: 3
|
109
|
+
summary: Entertaining fixtures for Rails
|
110
|
+
test_files: []
|