├── .gitignore ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib ├── traver.rb └── traver │ ├── attributes_resolver.rb │ ├── attributes_resolvers │ ├── active_record_attributes_resolver.rb │ └── poro_attributes_resolver.rb │ ├── default_params_creators │ ├── active_record_default_params_creator.rb │ └── poro_default_params_creator.rb │ ├── factories_loader.rb │ ├── factories_store.rb │ ├── factory.rb │ ├── graph.rb │ ├── graph_creator.rb │ ├── list_creator.rb │ ├── object_creator.rb │ ├── object_persisters │ ├── active_record_object_persister.rb │ └── poro_object_persister.rb │ ├── sequencer.rb │ ├── settings.rb │ ├── settings │ ├── active_record_settings.rb │ └── poro_settings.rb │ ├── traver_constructor.rb │ └── version.rb ├── test ├── integration │ ├── active_record_test.rb │ └── poro_test.rb ├── support │ ├── class_definer.rb │ └── model_definer.rb ├── test_helper.rb ├── traver_test.rb └── unit │ ├── factories_loader_test.rb │ ├── factories_store_test.rb │ ├── factory_test.rb │ ├── graph_creator_test.rb │ ├── graph_test.rb │ ├── list_creator_test.rb │ ├── object_creator_test.rb │ ├── sequencer_test.rb │ └── traver_constructor_test.rb └── traver.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | test.db -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3-p551 4 | - 2.1.8 5 | - 2.2.4 6 | - 2.3.0 7 | - rbx-3.14 8 | before_install: gem install bundler -v 1.11.2 9 | 10 | notifications: 11 | email: 12 | on_success: never 13 | on_failure: always -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in traver.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Yuri Kaspervich 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Traver [![Build Status](https://travis-ci.org/yukas/traver.svg?branch=master)](https://travis-ci.org/yukas/traver) 2 | 3 | ## Advantages 4 | #### Concise syntax 5 | 6 | FactoryGirl: 7 | ```ruby 8 | user = FactoryGirl.create(:user) 9 | blog = FactoryGirl.create(:blog, user: user) 10 | posts = FactoryGirl.create_list(:post, 2, blog: blog, user: user) 11 | ``` 12 | 13 | Traver: 14 | ```ruby 15 | Traver.create(:user, blog: { posts: 2 }) 16 | ``` 17 | 18 | #### Ability to setup data inside specs 19 | 20 | Thanks to concise syntax, you're able to setup data inside the spec itself, opposite to factory file, so you'll see all setup at a glance. 21 | 22 | #### No centralized setup 23 | 24 | As soon as all setup happening in the spec itself, no more braking specs when you change one factory. 25 | 26 | ## Installation 27 | 28 | ```shell 29 | gem install traver 30 | ``` 31 | 32 | or add the following line to Gemfile: 33 | 34 | ```ruby 35 | gem 'traver' 36 | ``` 37 | 38 | and run `bundle install` from your shell. 39 | 40 | ## Usage 41 | 42 | Create object with attributes: 43 | 44 | ```ruby 45 | blog = Traver.create(blog: { title: "Blog" }) #=> # 46 | ``` 47 | 48 | Define and use factories: 49 | 50 | ```ruby 51 | Traver.factories do 52 | factory :user, { 53 | full_name: "Walter White" 54 | } 55 | 56 | factory :post, { 57 | title: "Hello" 58 | } 59 | end 60 | 61 | Traver.create(:user) #=> # 62 | Traver.create(:post) #=> # 63 | ``` 64 | 65 | Define child factories: 66 | 67 | ```ruby 68 | Traver.factories do 69 | factory :post, { 70 | title: "Hello" 71 | } 72 | 73 | factory :published_post, :post, { 74 | published: true 75 | } 76 | 77 | factory :draft_post, :post, { 78 | published: false 79 | } 80 | end 81 | 82 | Traver.create(:published_post) #=> # 83 | Traver.create(:draft_post) #=> # 84 | ``` 85 | 86 | Create associated objects: 87 | 88 | ```ruby 89 | blog = Traver.create(blog: { 90 | title: "Hello", 91 | user: { name: "Mike" } 92 | }) 93 | 94 | blog.user #=> # 95 | ``` 96 | 97 | Create associated objects using factory names: 98 | 99 | ```ruby 100 | Traver.factory(:mike, :user, { 101 | name: "Mike" 102 | }) 103 | 104 | blog = Traver.create(blog: { 105 | title: "Hello", 106 | user: :mike 107 | }) 108 | 109 | ``` 110 | 111 | Create associated collections: 112 | 113 | ```ruby 114 | blog = Traver.create(blog: { 115 | title: "Hello", 116 | posts: [ 117 | { title: "Post #1" }, 118 | { title: "Post #2" } 119 | ] 120 | }) 121 | 122 | blog.posts #=> [#, #] 123 | 124 | ``` 125 | 126 | Create associated collections using numbers: 127 | 128 | ```ruby 129 | Traver.create(blog: { title: "Hello", posts: 2 }) 130 | Traver.create(blog: { title: "Hello", posts: [2, title: "Post #${n}"] }) 131 | Traver.create(blog: { title: "Hello", posts: [2, :published_post] }) 132 | ``` 133 | 134 | Create associated collections using factory names: 135 | 136 | ```ruby 137 | Traver.create(blog: { title: "Hello", posts: [:published_post, :draft_post] }) 138 | ``` 139 | 140 | Create associated with already existing objects: 141 | 142 | ```ruby 143 | Traver.create(blog: { title: "Hello", posts: [post1, post2] }) 144 | ``` 145 | 146 | ### Reusing associations 147 | 148 | Traver reuses already created objects for similar associations: 149 | 150 | ```ruby 151 | class Blog 152 | belongs_to :user 153 | has_many :posts 154 | end 155 | 156 | class Post 157 | belongs_to :user 158 | end 159 | ``` 160 | ```ruby 161 | blog = Traver.create(blog, posts: 1) # => blog.user == blog.posts.first.user 162 | ``` 163 | 164 | We can explicitly specify to create separate users for blog and a post: 165 | 166 | ```ruby 167 | blog = Traver.create(blog, posts: [ { user: 1 } ]) # => blog.user != blog.posts.first.user 168 | ``` 169 | 170 | Create lists with sequences: 171 | 172 | ```ruby 173 | users = Traver.create_list(2, user: { email: "user${n}@mail.me" }) 174 | #=> [#, #] 175 | 176 | users = Traver.create_list(2, :published_post) 177 | #=> [#, #] 178 | ``` 179 | 180 | Graph is a convenient way to reference created objects: 181 | 182 | ```ruby 183 | graph = Traver.create_graph(blog: { posts: [{ tags: 2 }] }) 184 | 185 | graph.blog #=> # 186 | 187 | graph.posts #=> [#] 188 | graph.post #=> # 189 | graph.post1 #=> # 190 | 191 | graph.tags #=> [#, #] 192 | graph.tag #=> # 193 | graph.tag1 #=> # 194 | graph.tag2 #=> # 195 | 196 | # Delegates attributes: 197 | graph.blog_title #=> "Hello" 198 | graph.blog1_title #=> "Hello" 199 | 200 | graph.post_tag_title #=> "Tag" 201 | graph.post1_tag1_title #=> "Tag" 202 | 203 | # Delegates methods: 204 | graph.tags_length #=> 2 205 | ``` 206 | 207 | Use procs for dynamic attribute values: 208 | 209 | ```ruby 210 | blog = Traver.create(event: { 211 | start_at: -> { 1.day.ago }, 212 | finish_at: -> object { object.start_at + 2.days } 213 | }) 214 | ``` 215 | 216 | Procs executed in the context of created object. 217 | 218 | ## Rails 219 | 220 | By default Traver loads factories from`test/factories.rb` or `spec/factories.rb` for rspec users. 221 | 222 | Objects for `belongs_to` associations are created automatically: 223 | 224 | ```ruby 225 | class Blog < ActiveRecord::Base 226 | has_many :blogs 227 | end 228 | 229 | class Post < ActiveRecord::Base 230 | belongs_to :blog 231 | end 232 | 233 | post = Traver.create(:post) #=> # 234 | post.blog #=> # 235 | ``` 236 | 237 | ## Plays well with FactoryGirl 238 | 239 | If you want to try out Traver for your new specs and keep using FactoryGirl for the old ones, no problem with that. Traver will detect FactoryGirl and will searching for factories inside `spec/traver_factories.rb` or `test/traver_factories.rb`. 240 | 241 | ## Development 242 | 243 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 244 | 245 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 246 | 247 | ## Contributing 248 | 249 | Bug reports and pull requests are welcome on GitHub at https://github.com/yukas/traver. 250 | 251 | 252 | ## License 253 | 254 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT). 255 | 256 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList['test/**/*_test.rb'] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "traver" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/traver.rb: -------------------------------------------------------------------------------- 1 | require "active_support/inflector" 2 | 3 | Dir["#{File.dirname(__FILE__)}/traver/**/*.rb"].each { |file| require file } 4 | 5 | module Traver 6 | class Error < Exception; end 7 | 8 | class << self 9 | def create(*args) 10 | load_factories 11 | 12 | traver_constructor.create(*args) 13 | end 14 | 15 | def create_graph(*args) 16 | load_factories 17 | 18 | traver_constructor.create_graph(*args) 19 | end 20 | 21 | def create_list(*args) 22 | load_factories 23 | 24 | traver_constructor.create_list(*args) 25 | end 26 | 27 | def define_factory(*args) 28 | traver_constructor.define_factory(*args) 29 | end 30 | 31 | def define_factories(&block) 32 | traver_constructor.define_factories(&block) 33 | end 34 | 35 | def undefine_all_factories 36 | traver_constructor.undefine_all_factories 37 | end 38 | 39 | alias :factory :define_factory 40 | alias :factories :define_factories 41 | 42 | private 43 | 44 | def traver_constructor 45 | @traver_constructor ||= TraverConstructor.new 46 | end 47 | 48 | def load_factories 49 | factories_loader.load_factories 50 | end 51 | 52 | def factories_loader 53 | @factories_loader ||= FactoriesLoader.new(Dir.getwd) 54 | end 55 | end 56 | end -------------------------------------------------------------------------------- /lib/traver/attributes_resolver.rb: -------------------------------------------------------------------------------- 1 | module Traver 2 | class AttributesResolver 3 | def select_attributes_params(params, object_class) 4 | params.select { |name, value| regular_attribute?(object_class, name, value) } 5 | end 6 | 7 | def select_objects_params(params, object_class) 8 | params.select { |name, value| nested_object?(object_class, name, value) } 9 | end 10 | 11 | def select_has_one_objects_params(params, object_class) 12 | params.select { |name, value| has_one_object?(object_class, name, value) } 13 | end 14 | 15 | def select_collections_params(object, factory, params) 16 | params.select { |name, value| nested_collection?(factory.object_class, name, value) } 17 | end 18 | 19 | private 20 | 21 | def regular_attribute?(object_class, name, value) 22 | !nested_object?(object_class, name, value) && 23 | !nested_collection?(object_class, name, value) && 24 | !has_one_object?(object_class, name, value) 25 | end 26 | end 27 | end -------------------------------------------------------------------------------- /lib/traver/attributes_resolvers/active_record_attributes_resolver.rb: -------------------------------------------------------------------------------- 1 | require "traver/attributes_resolver" 2 | 3 | module Traver 4 | class ActiveRecordAttributesResolver < AttributesResolver 5 | 6 | private 7 | 8 | def nested_object?(object_class, name, value) 9 | reflection = object_class.reflect_on_association(name) 10 | reflection && reflection.macro == :belongs_to 11 | end 12 | 13 | def has_one_object?(object_class, name, value) 14 | reflection = object_class.reflect_on_association(name) 15 | reflection && reflection.macro == :has_one 16 | end 17 | 18 | def nested_collection?(object_class, name, value) 19 | reflection = object_class.reflect_on_association(name) 20 | reflection && reflection.collection? 21 | end 22 | end 23 | end -------------------------------------------------------------------------------- /lib/traver/attributes_resolvers/poro_attributes_resolver.rb: -------------------------------------------------------------------------------- 1 | require "traver/attributes_resolver" 2 | 3 | module Traver 4 | class PoroAttributesResolver < AttributesResolver 5 | 6 | private 7 | 8 | def nested_object?(object_class, name, value) 9 | Object.const_defined?(name.to_s.camelize) 10 | end 11 | 12 | def has_one_object?(object_class, name, value) 13 | nested_object?(object_class, name, value) 14 | end 15 | 16 | def nested_collection?(object_class, name, value) 17 | if plural?(name) 18 | Object.const_defined?(name.to_s.singularize.camelize) 19 | end 20 | end 21 | 22 | def plural?(val) 23 | val.to_s.pluralize == val.to_s 24 | end 25 | end 26 | end -------------------------------------------------------------------------------- /lib/traver/default_params_creators/active_record_default_params_creator.rb: -------------------------------------------------------------------------------- 1 | module Traver 2 | class ActiveRecordDefaultParamsCreator 3 | def default_params(object_class) 4 | associations = object_class.reflect_on_all_associations(:belongs_to) 5 | 6 | associations.each_with_object({}) do |association, result| 7 | result[association.name] = :__ref__ 8 | end 9 | end 10 | end 11 | end -------------------------------------------------------------------------------- /lib/traver/default_params_creators/poro_default_params_creator.rb: -------------------------------------------------------------------------------- 1 | module Traver 2 | class PoroDefaultParamsCreator 3 | def default_params(object_class) 4 | {} 5 | end 6 | end 7 | end -------------------------------------------------------------------------------- /lib/traver/factories_loader.rb: -------------------------------------------------------------------------------- 1 | module Traver 2 | class FactoriesLoader 3 | attr_reader :base_dir, :folder_name 4 | attr_reader :factories_loaded 5 | 6 | FOLDER_NAMES = ["test", "spec"] 7 | FILE_NAMES = ["factories.rb", "traver_factories.rb"] 8 | 9 | def initialize(base_dir) 10 | @base_dir = base_dir 11 | 12 | @factories_loaded = false 13 | end 14 | 15 | def load_factories 16 | unless factories_loaded 17 | FOLDER_NAMES.each do |folder_name| 18 | FILE_NAMES.each do |file_name| 19 | load_file(folder_name, file_name) 20 | end 21 | end 22 | 23 | @factories_loaded = true 24 | end 25 | end 26 | 27 | private 28 | 29 | def load_file(folder_name, file_name) 30 | file_path = File.join(base_dir, folder_name, file_name) 31 | 32 | if file_class.exist?(file_path) 33 | kernel_module.require file_path 34 | end 35 | end 36 | 37 | def kernel_module 38 | Kernel 39 | end 40 | 41 | def file_class 42 | File 43 | end 44 | end 45 | end -------------------------------------------------------------------------------- /lib/traver/factories_store.rb: -------------------------------------------------------------------------------- 1 | module Traver 2 | class FactoriesStore 3 | def initialize 4 | undefine_all_factories 5 | end 6 | 7 | def define_factory(factory_name, parent_name, params) 8 | factories[factory_name] = Factory.new(factory_name, params, factory_by_name(parent_name)) 9 | end 10 | 11 | def factory_defined?(factory_name) 12 | factories.has_key?(factory_name) 13 | end 14 | 15 | def factory_by_name(factory_name) 16 | if factory_name 17 | factories[factory_name] || empty_factory(factory_name) 18 | end 19 | end 20 | 21 | def undefine_all_factories 22 | @factories = {} 23 | end 24 | 25 | def factories_count 26 | factories.keys.length 27 | end 28 | 29 | alias :factory :define_factory 30 | 31 | private 32 | attr_reader :factories 33 | 34 | def empty_factory(factory_name) 35 | Factory.new(factory_name, {}, nil) 36 | end 37 | end 38 | end -------------------------------------------------------------------------------- /lib/traver/factory.rb: -------------------------------------------------------------------------------- 1 | module Traver 2 | class Factory 3 | attr_reader :name, :params, :parent_factory 4 | 5 | def initialize(name, params, parent_factory) 6 | @name = name 7 | @params = params 8 | @parent_factory = parent_factory 9 | end 10 | 11 | def root_factory 12 | if parent_factory 13 | parent_factory.root_factory 14 | else 15 | self 16 | end 17 | end 18 | 19 | def root_name 20 | root_factory.name 21 | end 22 | 23 | def inherited_params 24 | if parent_factory 25 | parent_factory.inherited_params.merge(params) 26 | else 27 | params 28 | end 29 | end 30 | 31 | def object_class_name 32 | root_factory.name.to_s.camelize 33 | end 34 | 35 | def object_class 36 | Object.const_get(object_class_name) 37 | end 38 | end 39 | end -------------------------------------------------------------------------------- /lib/traver/graph.rb: -------------------------------------------------------------------------------- 1 | module Traver 2 | class Graph 3 | def initialize 4 | @vertices = {} 5 | end 6 | 7 | def add_vertex(key, object) 8 | vertices[key] ||= [] 9 | vertices[key] << object 10 | end 11 | 12 | def [](key) 13 | key = key.to_s 14 | 15 | if plural?(key) 16 | vertices[key.singularize.to_sym] 17 | else 18 | name, index = key.split(/(\d+)$/) 19 | index = (index || 1).to_i 20 | 21 | vertices[name.to_sym][index - 1] 22 | end 23 | end 24 | 25 | def method_missing(method_name, *args) 26 | self[method_name] 27 | end 28 | 29 | private 30 | attr_reader :vertices 31 | 32 | def plural?(value) 33 | value == value.pluralize 34 | end 35 | end 36 | end -------------------------------------------------------------------------------- /lib/traver/graph_creator.rb: -------------------------------------------------------------------------------- 1 | module Traver 2 | class GraphCreator 3 | attr_reader :factory_name, :params, :factories_store, :sequencer 4 | attr_reader :graph 5 | 6 | def initialize(factory_name, params, factories_store, sequencer) 7 | @factory_name = factory_name 8 | @params = params 9 | @factories_store = factories_store 10 | @sequencer = sequencer 11 | 12 | @object_creator = ObjectCreator.new(factory_name, params, factories_store, sequencer) 13 | 14 | @graph = Graph.new 15 | end 16 | 17 | def create_graph 18 | object_creator.after_create = lambda do |creator| 19 | graph.add_vertex(creator.factory_name, creator.object) 20 | end 21 | 22 | object_creator.create_object 23 | end 24 | 25 | private 26 | attr_reader :object_creator 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/traver/list_creator.rb: -------------------------------------------------------------------------------- 1 | module Traver 2 | class ListCreator 3 | attr_reader :num, :factory_name, :params, :factories_store, :sequencer 4 | attr_reader :list 5 | 6 | def initialize(num, factory_name, params, factories_store, sequencer) 7 | @num = num 8 | @factory_name = factory_name 9 | @params = params 10 | @factories_store = factories_store 11 | @sequencer = sequencer 12 | end 13 | 14 | def create_list 15 | @list = num.times.map do 16 | object_creator.create_object(factory_name, params, factories_store, sequencer) 17 | end 18 | end 19 | 20 | private 21 | 22 | def object_creator 23 | ObjectCreator 24 | end 25 | end 26 | end -------------------------------------------------------------------------------- /lib/traver/object_creator.rb: -------------------------------------------------------------------------------- 1 | require "forwardable" 2 | 3 | module Traver 4 | class ObjectCreator 5 | extend Forwardable 6 | 7 | attr_reader :factory_name, :params, :factory_definer, :sequencer, :cache, :nesting 8 | attr_reader :object 9 | 10 | attr_accessor :after_create 11 | 12 | def_delegators :settings, :object_persister, 13 | :attributes_resolver, 14 | :default_params_creator 15 | 16 | def self.create_object(factory_name, params, factory_definer, sequencer, cache = {}, nesting = 1) 17 | creator = new(factory_name, params, factory_definer, sequencer, cache, nesting) 18 | creator.create_object 19 | 20 | creator.object 21 | end 22 | 23 | def initialize(factory_name, params, factory_definer, sequencer, cache = {}, nesting = 1) 24 | @factory_name = factory_name 25 | @params = params 26 | @factory_definer = factory_definer 27 | @sequencer = sequencer 28 | @cache = cache 29 | @nesting = nesting 30 | end 31 | 32 | def create_object 33 | obtain_factory 34 | 35 | if obtain_object_from_cache? 36 | obtain_object_from_cache 37 | else 38 | # puts "#{'-' * nesting} #{factory_name}
" 39 | 40 | change_ref_to_be_empty_hash 41 | merge_factory_params 42 | merge_default_params 43 | instantiate_object 44 | set_nested_objects 45 | set_attributes 46 | persist_object 47 | cache_object 48 | set_has_one_objects 49 | set_nested_collections 50 | call_after_create_hook 51 | end 52 | end 53 | 54 | private 55 | attr_reader :factory 56 | 57 | def obtain_factory 58 | @factory = factory_definer.factory_by_name(factory_name) 59 | end 60 | 61 | def obtain_object_from_cache? 62 | params == :__ref__ && cache.has_key?(factory.root_name) 63 | end 64 | 65 | def obtain_object_from_cache 66 | @object = cache[factory.root_name] 67 | end 68 | 69 | def change_ref_to_be_empty_hash 70 | @params = {} if params == :__ref__ 71 | end 72 | 73 | def merge_factory_params 74 | @params = factory.inherited_params.merge(params) 75 | end 76 | 77 | def merge_default_params 78 | @params = default_params_creator.default_params(factory.object_class).merge(params) 79 | end 80 | 81 | def instantiate_object 82 | @object = factory.object_class.new 83 | end 84 | 85 | # Attributes 86 | 87 | def set_attributes 88 | attributes_resolver.select_attributes_params(params, factory.object_class).each do |name, value| 89 | set_attribute(name, value) 90 | end 91 | end 92 | 93 | def set_attribute(attribute, value) 94 | if value.is_a?(Proc) 95 | value = 96 | if value.arity == 0 97 | value.call 98 | elsif value.arity == 1 99 | value.call(object) 100 | else 101 | raise "Parameter block can have either 0 or 1 argument" 102 | end 103 | elsif value.is_a?(String) 104 | if sequencer.value_has_sequence?(value) 105 | value = sequencer.interpolate_sequence(attribute, value) 106 | end 107 | end 108 | 109 | object.public_send("#{attribute}=", value) 110 | end 111 | 112 | # Nested Objects 113 | 114 | def set_nested_objects 115 | attributes_resolver.select_objects_params(params, factory.object_class).each do |name, value| 116 | set_nested_object(name, value) 117 | end 118 | end 119 | 120 | def set_nested_object(name, value) 121 | if value.is_a?(Integer) 122 | set_attribute(name, create_nested_object(name, {})) 123 | elsif value.is_a?(Hash) || value == :__ref__ 124 | set_attribute(name, create_nested_object(name, value)) 125 | elsif value.is_a?(Symbol) 126 | set_attribute(name, create_nested_object(value, {})) 127 | else 128 | set_attribute(name, value) 129 | cache[name] = value 130 | end 131 | end 132 | 133 | def create_nested_object(factory_name, params) 134 | object_creator = ObjectCreator.new(factory_name, params, factory_definer, sequencer, cache, nesting + 1) 135 | object_creator.after_create = after_create 136 | object_creator.create_object 137 | 138 | object_creator.object 139 | end 140 | 141 | 142 | # Has one objects 143 | 144 | def set_has_one_objects 145 | attributes_resolver.select_has_one_objects_params(params, factory.object_class).each do |name, value| 146 | set_nested_object(name, value) 147 | end 148 | end 149 | 150 | # Nested Collections 151 | 152 | def set_nested_collections 153 | attributes_resolver.select_collections_params(object, factory, params).each do |collection_name, params_array| 154 | set_nested_collection(collection_name, params_array) 155 | end 156 | end 157 | 158 | def set_nested_collection(collection_name, params_array) 159 | factory_name = collection_name.to_s.singularize.to_sym 160 | 161 | set_attribute(collection_name, create_nested_collection(factory_name, params_array)) 162 | end 163 | 164 | def create_nested_collection(factory_name, params_array) 165 | if params_array.is_a?(Integer) 166 | params_array = [{}] * params_array 167 | 168 | params_array.map do |params| 169 | create_nested_object(factory_name, params) 170 | end 171 | elsif params_array.first == :__ref__ 172 | params_array.map do |_| 173 | create_nested_object(factory_name, :__ref__) 174 | end 175 | elsif params_array.first.is_a?(Symbol) 176 | params_array.map do |factory_name| 177 | create_nested_object(factory_name, {}) 178 | end 179 | else 180 | if params_array.first.is_a?(Integer) 181 | num, hash_or_symbol = params_array 182 | 183 | if hash_or_symbol.nil? 184 | params_array = [{}] * num 185 | elsif (hash = hash_or_symbol).is_a?(Hash) 186 | params_array = [hash] * num 187 | elsif hash_or_symbol.is_a?(Symbol) 188 | factory_name = hash_or_symbol 189 | 190 | params_array = [{}] * num 191 | else 192 | raise "Wrong collection params #{params_array}" 193 | end 194 | 195 | params_array.map do |params| 196 | create_nested_object(factory_name, params) 197 | end 198 | elsif params_array.first.is_a?(Hash) 199 | params_array.map do |params| 200 | create_nested_object(factory_name, params) 201 | end 202 | else 203 | params_array 204 | end 205 | end 206 | end 207 | 208 | # Settings 209 | 210 | 211 | def settings 212 | @settings ||= 213 | if object_is_active_record? 214 | ActiveRecordSettings.new 215 | else 216 | PoroSettings.new 217 | end 218 | end 219 | 220 | def object_is_active_record? 221 | factory.object_class < ActiveRecord::Base 222 | end 223 | 224 | 225 | # Persist Object 226 | 227 | def persist_object 228 | object_persister.persist_object(object) 229 | end 230 | 231 | def cache_object 232 | cache[factory.root_name] = object 233 | end 234 | 235 | 236 | # Hooks 237 | 238 | def call_after_create_hook 239 | after_create.call(self) if after_create 240 | end 241 | end 242 | end 243 | -------------------------------------------------------------------------------- /lib/traver/object_persisters/active_record_object_persister.rb: -------------------------------------------------------------------------------- 1 | module Traver 2 | class ActiveRecordObjectPersister 3 | def persist_object(object) 4 | unless object.save 5 | raise "Unable to save #{object.class}: #{object.errors.full_messages}" 6 | end 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /lib/traver/object_persisters/poro_object_persister.rb: -------------------------------------------------------------------------------- 1 | module Traver 2 | class PoroObjectPersister 3 | def persist_object(object) 4 | end 5 | end 6 | end -------------------------------------------------------------------------------- /lib/traver/sequencer.rb: -------------------------------------------------------------------------------- 1 | class Sequencer 2 | attr_reader :sequence_store 3 | 4 | def initialize 5 | @sequence_store = {} 6 | end 7 | 8 | def value_has_sequence?(value) 9 | !!(value =~ /\$\{n\}/) 10 | end 11 | 12 | def interpolate_sequence(name, value) 13 | value.sub("${n}", next_sequence_value_for(name).to_s) 14 | end 15 | 16 | private 17 | 18 | def next_sequence_value_for(name) 19 | sequence_store[name] ||= 0 20 | sequence_store[name] += 1 21 | 22 | sequence_store[name] 23 | end 24 | end -------------------------------------------------------------------------------- /lib/traver/settings.rb: -------------------------------------------------------------------------------- 1 | module Traver 2 | class Settings 3 | attr_reader :object_persister, 4 | :attributes_resolver, 5 | :default_params_creator 6 | end 7 | end -------------------------------------------------------------------------------- /lib/traver/settings/active_record_settings.rb: -------------------------------------------------------------------------------- 1 | require "traver/settings" 2 | 3 | module Traver 4 | class ActiveRecordSettings < Settings 5 | def initialize 6 | super 7 | 8 | @object_persister = ActiveRecordObjectPersister.new 9 | @attributes_resolver = ActiveRecordAttributesResolver.new 10 | @default_params_creator = ActiveRecordDefaultParamsCreator.new 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /lib/traver/settings/poro_settings.rb: -------------------------------------------------------------------------------- 1 | require "traver/settings" 2 | 3 | module Traver 4 | class PoroSettings < Settings 5 | def initialize 6 | super 7 | 8 | @object_persister = PoroObjectPersister.new 9 | @attributes_resolver = PoroAttributesResolver.new 10 | @default_params_creator = PoroDefaultParamsCreator.new 11 | end 12 | end 13 | end -------------------------------------------------------------------------------- /lib/traver/traver_constructor.rb: -------------------------------------------------------------------------------- 1 | module Traver 2 | class TraverConstructor 3 | attr_reader :factories_store, :sequencer 4 | 5 | def initialize 6 | @factories_store = FactoriesStore.new 7 | @sequencer = Sequencer.new 8 | end 9 | 10 | def include(*modules) 11 | modules.each do |mod| 12 | self.class.send(:include, mod) 13 | end 14 | end 15 | 16 | def create(*options) 17 | factory_name, params = parse_create_options(options) 18 | 19 | ObjectCreator.create_object(factory_name, params, factories_store, sequencer) 20 | end 21 | 22 | def create_graph(*options) 23 | factory_name, params = parse_create_options(options) 24 | 25 | graph_creator = GraphCreator.new(factory_name, params, factories_store, sequencer) 26 | graph_creator.create_graph 27 | 28 | graph_creator.graph 29 | end 30 | 31 | def create_list(num, *options) 32 | factory_name, params = parse_create_options(options) 33 | 34 | list_creator = ListCreator.new(num, factory_name, params, factories_store, sequencer) 35 | list_creator.create_list 36 | 37 | list_creator.list 38 | end 39 | 40 | def define_factories(&block) 41 | instance_exec(&block) 42 | end 43 | 44 | alias :factories :define_factories 45 | 46 | def define_factory(factory_name, *options) 47 | parent_factory_name, params = parse_factory_options(options) 48 | 49 | factories_store.define_factory(factory_name, parent_factory_name, params) 50 | end 51 | 52 | alias :factory :define_factory 53 | 54 | def undefine_all_factories 55 | factories_store.undefine_all_factories 56 | end 57 | 58 | def factories_count 59 | factories_store.factories_count 60 | end 61 | 62 | private 63 | 64 | def parse_create_options(options) 65 | if factory_girl_options?(options) 66 | parse_factory_girl_options(options) 67 | else 68 | parse_traver_options(options) 69 | end 70 | end 71 | 72 | def factory_girl_options?(options) 73 | options.first.is_a?(Symbol) 74 | end 75 | 76 | def parse_factory_girl_options(options) 77 | factory_name, params = options 78 | params ||= {} 79 | 80 | [factory_name, params] 81 | end 82 | 83 | def parse_traver_options(options) 84 | options.first.first 85 | end 86 | 87 | def parse_factory_options(options) 88 | parent_factory_name = nil 89 | 90 | if options.size == 1 91 | params = options.first 92 | elsif options.size == 2 93 | parent_factory_name, params = options 94 | end 95 | 96 | [parent_factory_name, params] 97 | end 98 | end 99 | end -------------------------------------------------------------------------------- /lib/traver/version.rb: -------------------------------------------------------------------------------- 1 | module Traver 2 | VERSION = "0.3.6" 3 | end 4 | -------------------------------------------------------------------------------- /test/integration/active_record_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ActiveRecordTest < TraverTest 4 | attr_reader :subject 5 | 6 | def setup 7 | super 8 | 9 | @subject = TraverConstructor.new 10 | end 11 | 12 | def teardown 13 | super 14 | 15 | subject.undefine_all_factories 16 | end 17 | 18 | def test_create_object 19 | define_model("Blog") 20 | 21 | blog = subject.create(:blog) 22 | 23 | assert_equal true, blog.persisted? 24 | end 25 | 26 | def test_create_object_with_attributes 27 | define_model("Blog", title: :string) 28 | 29 | blog = subject.create(blog: { title: "Hello" }) 30 | 31 | assert_equal "Hello", blog.title 32 | assert_equal true, blog.persisted? 33 | end 34 | 35 | def test_create_object_using_factory 36 | define_model("Post", title: :string) 37 | subject.define_factory(:post, { title: "Hello" }) 38 | 39 | post = subject.create(:post) 40 | 41 | assert_equal "Hello", post.title 42 | assert_equal true, post.persisted? 43 | end 44 | 45 | def test_create_object_using_child_factory 46 | define_model("Post", title: :string, published: :boolean) 47 | 48 | subject.define_factory(:post, { title: "Hello" }) 49 | subject.define_factory(:published_post, :post, { published: true }) 50 | 51 | post = subject.create(:published_post) 52 | 53 | assert_equal "Hello", post.title 54 | assert_equal true, post.published 55 | 56 | assert_equal true, post.persisted? 57 | end 58 | 59 | def test_create_associated_object 60 | define_model("User", name: :string) 61 | 62 | define_model("Blog", user_id: :integer, title: :string) do 63 | belongs_to :user 64 | end 65 | 66 | blog = subject.create(blog: { 67 | title: "Hello", 68 | user: { name: "Mike" } 69 | }) 70 | 71 | assert_equal "Hello", blog.title 72 | assert_equal true, blog.persisted? 73 | 74 | assert_equal "Mike", blog.user.name 75 | assert_equal true, blog.user.persisted? 76 | end 77 | 78 | def test_create_associated_collection 79 | define_model("Blog", title: :string) do 80 | has_many :posts 81 | end 82 | 83 | define_model("Post", blog_id: :integer, title: :string) do 84 | belongs_to :blog 85 | end 86 | 87 | blog = subject.create(blog: { 88 | title: "Hello", 89 | posts: [ 90 | { title: "Post #1" }, 91 | { title: "Post #2" } 92 | ] 93 | }) 94 | 95 | assert_equal "Hello", blog.title 96 | assert_equal true, blog.persisted? 97 | 98 | assert_equal "Post #1", blog.posts.first.title 99 | assert_equal true, blog.posts.first.persisted? 100 | 101 | assert_equal "Post #2", blog.posts.last.title 102 | assert_equal true, blog.posts.last.persisted? 103 | end 104 | 105 | def test_create_collection_using_digit 106 | define_model("Blog") do 107 | has_many :posts 108 | end 109 | 110 | define_model("Post", blog_id: :integer) do 111 | belongs_to :blog 112 | end 113 | 114 | blog = subject.create(blog: { 115 | posts: 2 116 | }) 117 | 118 | assert_equal 2, blog.posts.length 119 | end 120 | 121 | def test_create_collection_using_array_of_objects 122 | define_model("Blog", title: :string) do 123 | has_many :posts 124 | end 125 | 126 | define_model("Post", blog_id: :integer, title: :string) do 127 | belongs_to :blog 128 | end 129 | 130 | blog = subject.create(:blog, posts: [ 131 | subject.create(:post, title: "Post #1"), subject.create(:post, title: "Post #2") 132 | ]) 133 | 134 | assert_equal "Post #1", blog.posts.first.title 135 | assert_equal "Post #2", blog.posts.last.title 136 | end 137 | 138 | def test_create_collection_using_reference 139 | define_model("Event", layout_id: :integer) do 140 | has_one :layout 141 | has_many :ticket_types 142 | end 143 | 144 | define_model("TicketType", event_id: :integer) do 145 | belongs_to :event 146 | end 147 | 148 | define_model("Layout", event_id: :integer, ticket_type_id: :integer) do 149 | belongs_to :event 150 | belongs_to :ticket_type 151 | end 152 | 153 | blog = subject.create(:event, ticket_types: [ :__ref__ ], layout: 1) 154 | 155 | assert_equal 1, TicketType.count 156 | end 157 | 158 | def test_any_level_of_nesting 159 | define_model("Blog", title: :string) do 160 | has_many :posts 161 | end 162 | 163 | define_model("Post", blog_id: :integer, title: :string) do 164 | belongs_to :blog 165 | has_many :tags 166 | end 167 | 168 | define_model("Tag", post_id: :integer, name: :string) do 169 | belongs_to :post 170 | end 171 | 172 | blog = subject.create(blog: { 173 | title: "Blog", 174 | posts: [{ 175 | title: "Hello", 176 | tags: [{ name: "Tag" }] 177 | }] 178 | }) 179 | 180 | assert_equal "Blog", blog.title 181 | assert_equal true, blog.persisted? 182 | 183 | assert_equal "Hello", blog.posts.first.title 184 | assert_equal true, blog.posts.first.persisted? 185 | 186 | assert_equal "Tag", blog.posts.first.tags.first.name 187 | assert_equal true, blog.posts.first.tags.first.persisted? 188 | end 189 | 190 | def test_assign_hash_if_no_association_exists 191 | define_model("Blog", title: :string, user: :string) do 192 | serialize :user, Hash 193 | end 194 | 195 | blog = subject.create(blog: { 196 | title: "Hello", 197 | user: { name: "Mike" } 198 | }) 199 | 200 | assert_equal Hash[{ name: "Mike" }], blog.user 201 | end 202 | 203 | def test_assign_array_if_no_association_exists 204 | define_model("Blog", title: :string, posts: :string) do 205 | serialize :posts, Array 206 | end 207 | 208 | blog = subject.create(blog: { 209 | title: "Blog", 210 | posts: [ 211 | { title: "Post" } 212 | ] 213 | }) 214 | 215 | assert_equal [{ title: "Post" }], blog.posts 216 | end 217 | 218 | def test_automatically_creates_belongs_to_objects 219 | define_model("Blog", title: :string) do 220 | has_many :posts 221 | end 222 | 223 | define_model("Post", blog_id: :integer, title: :string) do 224 | belongs_to :blog 225 | end 226 | 227 | post = subject.create(:post) 228 | 229 | assert_equal true, post.persisted? 230 | assert_equal true, post.blog.persisted? 231 | end 232 | 233 | def test_reuse_objects_for_belongs_to_associations 234 | define_model("User", name: :string) do 235 | has_many :emails 236 | end 237 | 238 | define_model("Email", user_id: :integer, address: :string) do 239 | belongs_to :user 240 | end 241 | 242 | subject.define_factory :user, { 243 | emails: [{ }] 244 | } 245 | 246 | subject.define_factory :email, { 247 | address: "walter@white.com" 248 | } 249 | 250 | user = subject.create(:user) 251 | 252 | assert_equal user.emails.first.user, user 253 | end 254 | 255 | def test_has_one_association_reuses_parent_object_in_its_belongs_to_association 256 | define_model("Car") do 257 | has_one :driver 258 | end 259 | 260 | define_model("Driver", car_id: :integer) do 261 | belongs_to :car 262 | end 263 | 264 | car = subject.create(car: { 265 | driver: 1 266 | }) 267 | 268 | assert_equal car.id, car.driver.car.id 269 | end 270 | 271 | def test_able_to_create_poro_object 272 | define_class("Blog", :title, :posts) 273 | define_class("Post", :title) 274 | 275 | result = subject.create_graph(blog: { 276 | title: "Hello", 277 | posts: [2, title: "Post ${n}"] 278 | }) 279 | 280 | assert_equal "Hello", result.blog.title 281 | assert_equal 2, result.posts.length 282 | assert_equal "Post 1", result.post1.title 283 | assert_equal "Post 2", result.post2.title 284 | end 285 | 286 | def test_balongs_to_association_can_take_objects 287 | define_model("Car", color: :string) do 288 | has_one :driver 289 | end 290 | 291 | define_model("Driver", car_id: :integer) do 292 | belongs_to :car 293 | end 294 | 295 | driver = subject.create(driver: { 296 | car: subject.create(car: { 297 | color: "Red" 298 | }) 299 | }) 300 | 301 | assert_equal "Red", driver.car.color 302 | assert_equal true, driver.persisted? 303 | assert_equal true, driver.car.persisted? 304 | end 305 | end -------------------------------------------------------------------------------- /test/integration/poro_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class PoroTest < TraverTest 4 | attr_reader :subject 5 | 6 | def setup 7 | super 8 | 9 | @subject = TraverConstructor.new 10 | end 11 | 12 | def teardown 13 | super 14 | 15 | subject.undefine_all_factories 16 | end 17 | 18 | def test_create_object 19 | define_class("Blog") 20 | 21 | blog = subject.create(:blog) 22 | 23 | assert_instance_of Blog, blog 24 | end 25 | 26 | def test_create_object_with_attributes 27 | define_class("Blog", :title) 28 | 29 | blog = subject.create(blog: { title: "Hello" }) 30 | 31 | assert_equal "Hello", blog.title 32 | end 33 | 34 | def test_create_object_with_a_factory 35 | define_class("Post", :title) 36 | subject.define_factory(:post, { title: "Hello" }) 37 | 38 | post = subject.create(:post) 39 | 40 | assert_equal "Hello", post.title 41 | end 42 | 43 | def test_create_object_with_a_child_factory 44 | define_class("Post", :title, :published) 45 | subject.define_factory(:post, { title: "Hello" }) 46 | subject.define_factory(:published_post, :post, { published: true }) 47 | 48 | post = subject.create(:published_post) 49 | 50 | assert_equal "Hello", post.title 51 | assert_equal true, post.published 52 | end 53 | 54 | def test_create_associated_object 55 | define_class("Blog", :title, :user) 56 | define_class("User", :name) 57 | 58 | blog = subject.create(blog: { 59 | title: "Hello", 60 | user: { name: "Mike" } 61 | }) 62 | 63 | assert_equal "Hello", blog.title 64 | assert_equal "Mike", blog.user.name 65 | end 66 | 67 | def test_create_associated_object_using_number 68 | define_class("Blog", :user) 69 | define_class("User") 70 | 71 | blog = subject.create(blog: { 72 | user: 1 73 | }) 74 | 75 | assert_instance_of User, blog.user 76 | end 77 | 78 | def test_create_associated_object_using_factory_name 79 | define_class("User", :blog) 80 | define_class("Blog", :title) 81 | 82 | subject.define_factory(:funky_blog, :blog, { 83 | title: "Funky title" 84 | }) 85 | 86 | user = subject.create(user: { 87 | blog: :funky_blog 88 | }) 89 | 90 | assert_equal "Funky title", user.blog.title 91 | end 92 | 93 | def test_create_associated_collection 94 | define_class("Blog", :posts) 95 | define_class("Post", :title) 96 | 97 | blog = subject.create(blog: { 98 | posts: [ 99 | { title: "Post #1" }, 100 | { title: "Post #2" } 101 | ] 102 | }) 103 | 104 | assert_equal "Post #1", blog.posts.first.title 105 | assert_equal "Post #2", blog.posts.last.title 106 | end 107 | 108 | def test_create_collection_with_a_number 109 | define_class("Blog", :posts) 110 | define_class("Post", :title) 111 | 112 | blog = subject.create(blog: { 113 | posts: 2 114 | }) 115 | 116 | assert_equal 2, blog.posts.length 117 | assert_instance_of Post, blog.posts.first 118 | assert_instance_of Post, blog.posts.last 119 | end 120 | 121 | def test_create_collection_with_number_and_params 122 | define_class("Blog", :posts) 123 | define_class("Post", :title) 124 | 125 | blog = subject.create(blog: { 126 | posts: [ 2, title: "Post" ] 127 | }) 128 | 129 | assert_equal "Post", blog.posts.first.title 130 | assert_equal "Post", blog.posts.last.title 131 | end 132 | 133 | def test_create_collection_using_number_and_factory_name 134 | define_class("Blog", :posts) 135 | define_class("Post", :published) 136 | 137 | subject.define_factory(:published_post, :post, { published: true }) 138 | 139 | blog = subject.create(blog: { 140 | posts: [ 2, :published_post ] 141 | }) 142 | 143 | assert_equal true, blog.posts.first.published 144 | assert_equal true, blog.posts.last.published 145 | end 146 | 147 | def test_create_collection_using_factory_names 148 | define_class("Blog", :posts) 149 | define_class("Post", :published) 150 | 151 | subject.define_factory(:published_post, :post, { published: true }) 152 | subject.define_factory(:draft_post, :post, { published: false }) 153 | 154 | blog = subject.create(blog: { 155 | posts: [ :published_post, :draft_post ] 156 | }) 157 | 158 | assert_equal true, blog.posts.first.published 159 | assert_equal false, blog.posts.last.published 160 | end 161 | 162 | def test_create_graph 163 | define_class("Blog", :title, :posts) 164 | define_class("Post", :title, :tags) 165 | define_class("Tag", :name) 166 | 167 | graph = subject.create_graph(blog: { 168 | title: "Blog", 169 | posts: [{ 170 | title: "Hello", 171 | tags: [{ name: "Tag" }] 172 | }] 173 | }) 174 | 175 | assert_equal "Blog", graph.blog.title 176 | 177 | assert_equal "Hello", graph.post.title 178 | assert_equal "Hello", graph.post1.title 179 | assert_equal "Hello", graph.posts.first.title 180 | 181 | assert_equal "Tag", graph.tag.name 182 | assert_equal "Tag", graph.tag1.name 183 | assert_equal "Tag", graph.tags.first.name 184 | end 185 | 186 | def test_assign_hash_instead_of_creating_association_object_if_no_constant_defined 187 | define_class("Blog", :title, :user) 188 | 189 | blog = subject.create(blog: { 190 | title: "Hello", 191 | user: { name: "Mike" } 192 | }) 193 | 194 | assert_equal Hash[{ name: "Mike" }], blog.user 195 | end 196 | 197 | def test_assign_array_instead_of_creating_collection_objects_if_no_constant_defined 198 | define_class("Blog", :title, :posts) 199 | 200 | blog = subject.create(blog: { 201 | title: "Blog", 202 | posts: [ 203 | { title: "Post" } 204 | ] 205 | }) 206 | 207 | assert_equal [{ title: "Post" }], blog.posts 208 | end 209 | 210 | def test_create_list 211 | define_class("User", :name) 212 | 213 | users = Traver.create_list(2, user: { name: "Walter" }) 214 | 215 | assert_equal 2, users.length 216 | assert_equal "Walter", users.first.name 217 | assert_equal "Walter", users.last.name 218 | end 219 | 220 | def test_sequence 221 | define_class("Blog", :posts) 222 | define_class("Post", :title) 223 | 224 | subject.define_factory(:post, { 225 | title: "Post ${n}" 226 | }) 227 | 228 | blog = subject.create(blog: { 229 | posts: 2 230 | }) 231 | 232 | assert_equal "Post 1", blog.posts.first.title 233 | assert_equal "Post 2", blog.posts.last.title 234 | end 235 | 236 | def test_sequences_for_different_attributes_are_different 237 | define_class("Blog", :posts, :tags) 238 | define_class("Post", :title) 239 | define_class("Tag", :name) 240 | 241 | subject.define_factory(:post, { 242 | title: "Post ${n}" 243 | }) 244 | 245 | subject.define_factory(:tag, { 246 | name: "Tag ${n}" 247 | }) 248 | 249 | blog = subject.create(blog: { 250 | posts: 2, 251 | tags: 2 252 | }) 253 | 254 | assert_equal "Post 1", blog.posts.first.title 255 | assert_equal "Post 2", blog.posts.last.title 256 | 257 | assert_equal "Tag 1", blog.tags.first.name 258 | assert_equal "Tag 2", blog.tags.last.name 259 | end 260 | 261 | def test_accept_proc_as_a_value_for_attribute 262 | define_class("Post", :published_at) 263 | time = DateTime.now 264 | 265 | DateTime.stub(:now, time) do 266 | post = subject.create(post: { 267 | published_at: -> { DateTime.now } 268 | }) 269 | 270 | assert_equal time, post.published_at 271 | end 272 | end 273 | 274 | def test_evaluate_proc_with_one_argument_which_is_created_object 275 | define_class("User", :first_name, :last_name, :full_name) 276 | 277 | user = subject.create(user: { 278 | first_name: "Walter", 279 | last_name: "White", 280 | full_name: -> o { "#{o.first_name} #{o.last_name}" } 281 | }) 282 | 283 | assert_equal "Walter White", user.full_name 284 | end 285 | 286 | def test_evaluate_proc_with_zero_arguments 287 | define_class("User", :first_name, :last_name, :full_name) 288 | 289 | full_name = "Walter White" 290 | 291 | user = subject.create(user: { 292 | first_name: "Walter", 293 | last_name: "White", 294 | full_name: -> { full_name } 295 | }) 296 | 297 | assert_equal "Walter White", user.full_name 298 | end 299 | 300 | def test_define_factories 301 | subject.define_factories do 302 | define_factory :user, { 303 | name: "Walter" 304 | } 305 | end 306 | 307 | assert_equal 1, subject.factories_count 308 | end 309 | 310 | def test_factories 311 | subject.factories do 312 | factory :user, { 313 | name: "Walter" 314 | } 315 | 316 | factory :blog, { 317 | title: "Hello" 318 | } 319 | end 320 | 321 | assert_equal 2, subject.factories_count 322 | end 323 | 324 | def test_support_factory_girl_syntax_for_create 325 | define_class("Blog", :title) 326 | 327 | blog = subject.create(:blog, title: "Hello") 328 | 329 | assert_equal "Hello", blog.title 330 | end 331 | 332 | def test_support_factory_girl_syntax_for_create_graph 333 | define_class("Blog", :title) 334 | 335 | graph = subject.create_graph(:blog, title: "Hello") 336 | 337 | assert_equal "Hello", graph.blog.title 338 | end 339 | 340 | def test_support_factory_girl_syntax_for_create_list 341 | define_class("Blog", :title) 342 | 343 | blogs = subject.create_list(2, :blog, title: "Hello") 344 | 345 | assert_equal 2, blogs.length 346 | assert_equal "Hello", blogs.first.title 347 | assert_equal "Hello", blogs.last.title 348 | end 349 | 350 | # def test_able_to_create_poro_and_ar_objects 351 | # define_model("User", name: :string) do 352 | # attr_accessor :blog 353 | # end 354 | # 355 | # define_class("Blog", :title) 356 | # 357 | # result = subject.create_graph(user: { 358 | # name: "Walter", 359 | # blog: { 360 | # title: "Hello" 361 | # } 362 | # }) 363 | # 364 | # assert_equal "Walter", result.user.name 365 | # assert_equal true, result.user.persisted? 366 | # assert_equal "Hello", result.blog.title 367 | # end 368 | end -------------------------------------------------------------------------------- /test/support/class_definer.rb: -------------------------------------------------------------------------------- 1 | class ClassDefiner 2 | attr_reader :defined_classes 3 | 4 | def initialize 5 | @defined_classes = [] 6 | end 7 | 8 | def define_class(class_name, *params) 9 | defined_class = if params.empty? 10 | Object.const_set(class_name, Class.new) 11 | else 12 | Object.const_set(class_name, Struct.new(*params)) 13 | end 14 | 15 | defined_classes << defined_class 16 | 17 | defined_class 18 | end 19 | 20 | def undefine_all_classes 21 | defined_classes.each do |klass| 22 | undefine_class(klass) 23 | end 24 | 25 | @defined_classes = [] 26 | end 27 | 28 | private 29 | 30 | def undefine_class(klass) 31 | Object.send(:remove_const, klass.name) 32 | end 33 | end 34 | 35 | if __FILE__ == $0 36 | require "bundler/setup" 37 | require "minitest/autorun" 38 | require "support/class_definer" 39 | 40 | class ClassDefinerTest < Minitest::Test 41 | attr_reader :subject 42 | 43 | def setup 44 | super 45 | 46 | @subject = ClassDefiner.new 47 | end 48 | 49 | def teardown 50 | super 51 | 52 | subject.undefine_all_classes 53 | end 54 | 55 | def test_define_class 56 | subject.define_class("Blog") 57 | 58 | assert defined?(Blog) 59 | end 60 | 61 | def test_define_class_with_attributes 62 | subject.define_class("Blog", :title) 63 | 64 | assert_includes Blog.instance_methods, :title 65 | end 66 | 67 | def test_undefine_all_classes 68 | subject.define_class("Blog") 69 | subject.define_class("Post") 70 | 71 | subject.undefine_all_classes 72 | 73 | refute defined?(Blog) 74 | refute defined?(Post) 75 | end 76 | end 77 | end -------------------------------------------------------------------------------- /test/support/model_definer.rb: -------------------------------------------------------------------------------- 1 | require "active_record" 2 | 3 | class ModelDefiner 4 | attr_reader :defined_models 5 | 6 | def initialize 7 | @defined_models = [] 8 | 9 | establish_connection 10 | end 11 | 12 | def define_model(model_name, attributes = {}, &block) 13 | defined_model = Object.const_set(model_name.to_s.camelize, Class.new(ActiveRecord::Base)) 14 | defined_models << defined_model 15 | 16 | create_table(defined_model.table_name) do |table| 17 | attributes.each do |attr_name, attr_type| 18 | table.column attr_name, attr_type 19 | end 20 | end 21 | 22 | defined_model.class_eval(&block) if block_given? 23 | end 24 | 25 | def undefine_all_models 26 | defined_models.each do |model| 27 | undefine_model(model) 28 | end 29 | 30 | @defined_models = [] 31 | end 32 | 33 | def undefine_model(model) 34 | drop_table(model.table_name) 35 | remove_constant(model.to_s) 36 | end 37 | 38 | private 39 | attr_reader :connection 40 | 41 | def establish_connection 42 | ActiveRecord::Base.establish_connection( 43 | adapter: "sqlite3", 44 | database: File.join(File.dirname(__FILE__), "test.db") 45 | ) 46 | 47 | @connection = ActiveRecord::Base.connection 48 | end 49 | 50 | def create_table(table_name, &block) 51 | begin 52 | connection.create_table(table_name, &block) 53 | rescue Exception => e 54 | drop_table(table_name) 55 | 56 | raise e 57 | end 58 | end 59 | 60 | def drop_table(table_name) 61 | connection.execute("DROP TABLE IF EXISTS #{table_name}") 62 | end 63 | 64 | def remove_constant(const_name) 65 | Object.send(:remove_const, const_name) 66 | end 67 | end 68 | 69 | if __FILE__ == $0 70 | require "bundler/setup" 71 | require "active_record" 72 | require "minitest/autorun" 73 | 74 | class ModelDefinerTest < Minitest::Test 75 | attr_reader :subject 76 | 77 | def setup 78 | super 79 | 80 | @subject = ModelDefiner.new 81 | end 82 | 83 | def teardown 84 | super 85 | 86 | subject.undefine_all_models 87 | end 88 | 89 | def test_define_model 90 | subject.define_model(:blog) 91 | 92 | assert defined?(Blog) 93 | assert_equal ActiveRecord::Base, Blog.superclass 94 | end 95 | 96 | def test_define_model_with_attributes 97 | subject.define_model(:blog, title: :string) 98 | 99 | assert_equal ["id", "title"], Blog.new.attributes.keys 100 | end 101 | 102 | def test_define_model_with_assocociation 103 | subject.define_model(:user) 104 | 105 | subject.define_model(:blog, user_id: :integer) do 106 | belongs_to :user 107 | end 108 | 109 | assert_equal :user, Blog.reflect_on_association(:user).name 110 | end 111 | 112 | def test_undefine_all_models 113 | subject.define_model(:user) 114 | subject.define_model(:blog) 115 | 116 | subject.undefine_all_models 117 | 118 | refute defined?(User) 119 | refute defined?(Blog) 120 | end 121 | end 122 | end -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require "minitest/autorun" 3 | require "support/class_definer" 4 | require "support/model_definer" 5 | require "traver_test" 6 | 7 | $LOAD_PATH.unshift File.expand_path("../../lib", __FILE__) 8 | require "traver" -------------------------------------------------------------------------------- /test/traver_test.rb: -------------------------------------------------------------------------------- 1 | class TraverTest < Minitest::Test 2 | include Traver 3 | 4 | attr_reader :class_definer, :model_definer 5 | 6 | def setup 7 | super 8 | 9 | @class_definer = ClassDefiner.new 10 | @model_definer = ModelDefiner.new 11 | end 12 | 13 | def define_class(*args) 14 | class_definer.define_class(*args) 15 | end 16 | 17 | def define_model(*args, &block) 18 | model_definer.define_model(*args, &block) 19 | end 20 | 21 | def teardown 22 | super 23 | 24 | class_definer.undefine_all_classes 25 | model_definer.undefine_all_models 26 | end 27 | end -------------------------------------------------------------------------------- /test/unit/factories_loader_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class FactoriesLoaderTest < TraverTest 4 | attr_reader :subject 5 | 6 | def setup 7 | super 8 | 9 | @subject = FactoriesLoader.new("/base/dir") 10 | end 11 | 12 | def test_load_factories 13 | file_class = MiniTest::Mock.new 14 | kernel_module = MiniTest::Mock.new 15 | 16 | 4.times { file_class.expect(:exist?, true, [String]) } 17 | 18 | kernel_module.expect(:require, Object, ["/base/dir/test/factories.rb"]) 19 | kernel_module.expect(:require, Object, ["/base/dir/test/traver_factories.rb"]) 20 | kernel_module.expect(:require, Object, ["/base/dir/spec/factories.rb"]) 21 | kernel_module.expect(:require, Object, ["/base/dir/spec/traver_factories.rb"]) 22 | 23 | subject.stub(:kernel_module, kernel_module) do 24 | subject.stub(:file_class, file_class) do 25 | subject.load_factories 26 | end 27 | end 28 | 29 | assert kernel_module.verify 30 | assert file_class.verify 31 | 32 | assert subject.factories_loaded 33 | end 34 | end -------------------------------------------------------------------------------- /test/unit/factories_store_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class FactoriesStoreTest < TraverTest 4 | attr_reader :subject 5 | 6 | def setup 7 | super 8 | 9 | @subject = FactoriesStore.new 10 | end 11 | 12 | def test_define_factory 13 | subject.define_factory(:post, nil, title: "Hello") 14 | 15 | assert subject.factory_defined?(:post) 16 | end 17 | 18 | def test_factory_by_name 19 | subject.define_factory(:post, nil, published: true) 20 | 21 | factory = subject.factory_by_name(:post) 22 | 23 | assert_equal Hash[{ published: true }], factory.params 24 | end 25 | 26 | def test_undefine_all_factories 27 | subject.define_factory(:post, nil, title: "Hello") 28 | 29 | subject.undefine_all_factories 30 | 31 | assert_equal 0, subject.factories_count 32 | end 33 | end -------------------------------------------------------------------------------- /test/unit/factory_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class FactoryTest < TraverTest 4 | def test_root_factory 5 | post_factory = Factory.new(:post, {}, nil) 6 | draft_post_factory = Factory.new(:draft_post, { published: false }, post_factory) 7 | 8 | assert_equal post_factory, draft_post_factory.root_factory 9 | end 10 | 11 | def test_root_name 12 | post_factory = Factory.new(:post, {}, nil) 13 | draft_post_factory = Factory.new(:draft_post, { published: false }, post_factory) 14 | 15 | assert_equal :post, draft_post_factory.root_name 16 | end 17 | 18 | def test_inherited_params 19 | post_factory = Factory.new(:post, { title: "Hello" }, nil) 20 | draft_post_factory = Factory.new(:draft_post, { published: false }, post_factory) 21 | 22 | assert_equal({ title: "Hello", published: false }, draft_post_factory.inherited_params) 23 | end 24 | end -------------------------------------------------------------------------------- /test/unit/graph_creator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class GraphCreatorTest < TraverTest 4 | def subject 5 | @subject ||= GraphCreator.new(:blog, { title: "Hello" }, FactoriesStore.new, Sequencer.new) 6 | end 7 | 8 | def test_create_graph 9 | define_class("Blog", :title) 10 | 11 | subject.create_graph 12 | 13 | assert_instance_of Blog, subject.graph.blog 14 | end 15 | end -------------------------------------------------------------------------------- /test/unit/graph_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class GraphTest < TraverTest 4 | attr_reader :subject 5 | 6 | def setup 7 | super 8 | 9 | @subject = Graph.new 10 | end 11 | 12 | def test_add_vertex 13 | subject.add_vertex(:blog, "Blog") 14 | subject.add_vertex(:post, "Post") 15 | 16 | assert_equal "Blog", subject.blog 17 | assert_equal "Post", subject.post 18 | end 19 | 20 | def test_add_vertex_with_identical_key 21 | subject.add_vertex(:blog, "Object #1") 22 | subject.add_vertex(:blog, "Object #2") 23 | 24 | assert_equal "Object #1", subject.blog 25 | assert_equal "Object #1", subject.blog1 26 | assert_equal "Object #2", subject.blog2 27 | end 28 | 29 | def test_vertices_collection 30 | subject.add_vertex(:blog, "Object #1") 31 | subject.add_vertex(:blog, "Object #2") 32 | 33 | assert_equal ["Object #1", "Object #2"], subject.blogs 34 | end 35 | 36 | def test_access_vertices_via_method_calls 37 | subject.add_vertex(:blog, "Object #1") 38 | 39 | assert_equal "Object #1", subject.blog 40 | end 41 | end -------------------------------------------------------------------------------- /test/unit/list_creator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ListCreatorTest < TraverTest 4 | attr_reader :subject 5 | attr_reader :object_creator 6 | 7 | def setup 8 | super 9 | 10 | define_class("User", :name) 11 | 12 | @object_creator = MiniTest::Mock.new 13 | 14 | @subject = ListCreator.new(2, :user, { name: "Walter" }, FactoriesStore.new, Sequencer.new) 15 | 16 | 2.times do 17 | object_creator.expect(:create_object, Object, [Symbol, Hash, FactoriesStore, Sequencer]) 18 | end 19 | end 20 | 21 | def test_create_list 22 | subject.stub(:object_creator, object_creator) do 23 | subject.create_list 24 | end 25 | 26 | assert object_creator.verify 27 | end 28 | 29 | def test_resulting_list_is_an_array 30 | subject.stub(:object_creator, object_creator) do 31 | subject.create_list 32 | 33 | assert Array, subject.list.class 34 | end 35 | end 36 | end -------------------------------------------------------------------------------- /test/unit/object_creator_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class ObjectCreatorTest < TraverTest 4 | def subject 5 | @subject ||= ObjectCreator.new(:blog, { title: "Hello" }, FactoriesStore.new, Sequencer.new) 6 | end 7 | 8 | def test_create_object 9 | define_class("Blog", :title) 10 | 11 | subject.create_object 12 | 13 | assert_equal "Hello", subject.object.title 14 | end 15 | 16 | def test_after_create_hook 17 | define_class("Blog", :title) 18 | 19 | object = nil 20 | 21 | subject.after_create = lambda do |creator| 22 | object = creator.object 23 | end 24 | 25 | subject.create_object 26 | 27 | assert_equal "Hello", object.title 28 | end 29 | end -------------------------------------------------------------------------------- /test/unit/sequencer_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class SequencerTest < TraverTest 4 | attr_reader :subject 5 | 6 | def setup 7 | super 8 | 9 | @subject = Sequencer.new 10 | end 11 | 12 | def test_value_has_sequence 13 | assert_equal true, subject.value_has_sequence?("Hello ${n}") 14 | end 15 | 16 | def test_value_has_no_sequence 17 | assert_equal false, subject.value_has_sequence?("Hello") 18 | end 19 | 20 | def test_increments_value 21 | assert_equal "qwe1@qwe.qwe", subject.interpolate_sequence(:email, "qwe${n}@qwe.qwe") 22 | assert_equal "qwe2@qwe.qwe", subject.interpolate_sequence(:email, "qwe${n}@qwe.qwe") 23 | end 24 | 25 | def test_each_term_has_separate_sequence 26 | assert_equal "qwe1@qwe.qwe", subject.interpolate_sequence(:email, "qwe${n}@qwe.qwe") 27 | assert_equal "qwe2@qwe.qwe", subject.interpolate_sequence(:email, "qwe${n}@qwe.qwe") 28 | 29 | assert_equal "Hello 1", subject.interpolate_sequence(:title, "Hello ${n}") 30 | assert_equal "Hello 2", subject.interpolate_sequence(:title, "Hello ${n}") 31 | end 32 | end -------------------------------------------------------------------------------- /test/unit/traver_constructor_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | class TraverConstructorTest < TraverTest 4 | attr_reader :subject 5 | 6 | def setup 7 | super 8 | 9 | @subject = TraverConstructor.new 10 | end 11 | 12 | module Foo 13 | def foo 14 | :foo 15 | end 16 | end 17 | 18 | def test_includes_module 19 | subject.include(Foo) 20 | 21 | assert_equal :foo, subject.foo 22 | end 23 | end -------------------------------------------------------------------------------- /traver.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'traver/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "traver" 8 | spec.version = Traver::VERSION 9 | spec.authors = ["Yuri Kaspervich"] 10 | spec.email = ["ykas.gg@gmail.com"] 11 | 12 | spec.summary = %q{Traver is an object creation framework} 13 | spec.description = %q{Traver is an object creation framework} 14 | spec.homepage = "http://github.com/yukas/traver" 15 | spec.license = "MIT" 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 18 | spec.bindir = "exe" 19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 20 | spec.require_paths = ["lib"] 21 | 22 | spec.add_runtime_dependency "activesupport" 23 | 24 | spec.add_development_dependency "bundler", "~> 1.11" 25 | spec.add_development_dependency "rake", "~> 10.0" 26 | spec.add_development_dependency "minitest", "~> 5.0" 27 | spec.add_development_dependency "activerecord", "~> 4.0" 28 | spec.add_development_dependency "sqlite3" 29 | end 30 | --------------------------------------------------------------------------------