scheherazade 0.0.1
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/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: []
|