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 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,3 @@
1
+ %w[log story character_builder extension].each do |lib|
2
+ require_relative lib
3
+ end
@@ -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,7 @@
1
+ module Scheherazade
2
+ module Extension
3
+ def imagine(attributes = nil)
4
+ story.imagine(self, attributes)
5
+ end
6
+ end
7
+ 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
@@ -0,0 +1,3 @@
1
+ module Scheherazade
2
+ VERSION = "0.0.1"
3
+ end
@@ -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: []