diff --git a/.travis.yml b/.travis.yml index a5c9d54..d18d1d2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,6 @@ env: - DB=postgres DB_USER=postgres DB_PASSWORD="" before_script: - psql -c 'create database factory_test;' -U postgres - # - crystal ./spec/support/sam.cr -- db:migrate + - crystal ./spec/support/sam.cr -- db:migrate script: make seq diff --git a/shard.yml b/shard.yml index 2586244..c364b75 100644 --- a/shard.yml +++ b/shard.yml @@ -1,19 +1,19 @@ name: factory -version: 0.1.3 +version: 0.1.4 authors: - Roman Kalnytskyi -crystal: 0.24.1 +crystal: 0.26.1 license: MIT + development_dependencies: minitest: github: ysbaddaden/minitest.cr version: "~> 0.4.0" - # TODO: uncomment after resolving issue with migrating jennifer to 0.24.1 crystal - # jennifer: - # github: imdrasil/jennifer.cr - # version: "~> 0.3.1" + jennifer: + github: imdrasil/jennifer.cr + branch: "master" pg: github: will/crystal-pg diff --git a/spec/factory/base_spec.cr b/spec/factory/base_spec.cr index 43944ca..0bc8305 100644 --- a/spec/factory/base_spec.cr +++ b/spec/factory/base_spec.cr @@ -76,11 +76,11 @@ describe Factory::Base do describe ".attributes" do it "returns hash with element of attributes type" do - expect(TestFactory.attributes.class).must_equal(Hash(String, String | Int32 | Float64 | Array(Int32))) + expect(TestFactory.attributes).must_be_instance_of(Hash(String, String | Int32 | Float64 | Array(Int32))) end it "returns hash with automatically generated type" do - expect(HumanFactory.attributes.class).must_equal(Hash(String, String)) + expect(HumanFactory.attributes).must_be_instance_of(Hash(String, String)) end it "includes only attrs" do @@ -110,14 +110,9 @@ describe Factory::Base do end describe ".build_attributes" do - it "adds given parameters to attributes" do - hash = SecondTestFactory.build_attributes({"f1" => "f1"}) - expect(hash["f1"]).must_equal("f1") - hash = SecondTestFactory.build_attributes({:f1 => "f1"}) - expect(hash["f1"]).must_equal("f1") - hash = SecondTestFactory.build_attributes({f1: "f1"}) - expect(hash["f1"]).must_equal("f1") - end + it { expect(SecondTestFactory.build_attributes({"f1" => "f1"})["v1"]).must_equal("v1") } + it { expect(SecondTestFactory.build_attributes({:f1 => "v1"})["f1"]).must_equal("v1") } + it { expect(SecondTestFactory.build_attributes({f1: "v1"})["f1"]).must_equal("v1") } it "adds trait's attributes" do hash = SecondTestFactory.build_attributes({} of String => String, ["nested"]) @@ -131,16 +126,18 @@ describe Factory::Base do end describe ".make_assigns" do + @object : Test? + + let(:object) { Factory.build_test } + it "assigns to given object" do - obj = Factory.build_test - SecondTestFactory.make_assigns(obj, [] of String) - expect(obj.f3).must_equal(0.64) + SecondTestFactory.make_assigns(object, [] of String) + expect(object.f3).must_equal(0.64) end it "assigns traits to given object" do - obj = Factory.build_test - SecondTestFactory.make_assigns(obj, ["nested"]) - expect(obj.f2).must_equal(-2) + SecondTestFactory.make_assigns(object, ["nested"]) + expect(object.f2).must_equal(-2) end end diff --git a/spec/factory/jennifer/base_spec.cr b/spec/factory/jennifer/base_spec.cr index 83144ed..6d62715 100644 --- a/spec/factory/jennifer/base_spec.cr +++ b/spec/factory/jennifer/base_spec.cr @@ -1,136 +1,136 @@ require "../../spec_helper" -# describe Factory::Jennifer::Base do -# let(:described_class) { Factory::Jennifer::Base } - -# before do -# ::Jennifer::Adapter.adapter.begin_transaction -# end - -# after do -# ::Jennifer::Adapter.adapter.rollback_transaction -# end - -# describe "%association" do -# it "uses factory's defined association" do -# film = Factory.create_custom_film([:bad, :hit]) -# expect(film.author.nil?).wont_equal(true) -# expect(film.author!.name).must_match(/Author \d*$/) -# end - -# it "uses trait's author if it is given" do -# film = Factory.create_fiction_film([:with_special_author]) -# expect(film.author.nil?).wont_equal(true) -# expect(film.author!.name).must_equal("Special Author") -# end - -# it "uses given overrides for factory" do -# film = Factory.create_fiction_film([:with_special_author]) -# expect(film.author!.name).must_equal("Special Author") -# end - -# it "uses parent association if current factory has no" do -# film = Factory.create_fiction_film -# expect(film.author.nil?).must_equal(false) -# end - -# it "creates object without association if there is no one" do -# film = Factory.create_film -# expect(film.author.nil?).must_equal(true) -# end -# end - -# describe "%factory_creators" do -# it "defines all create methods on module level" do -# expect(Factory.create_custom_film.new_record?).must_equal(false) -# expect(Factory.create_custom_film(name: "New film").new_record?).must_equal(false) -# expect(Factory.create_custom_film({:name => "New"}).new_record?).must_equal(false) -# expect(Factory.create_custom_film([:bad]).new_record?).must_equal(false) -# expect(Factory.create_custom_film([:bad], name: "new").new_record?).must_equal(false) -# expect(Factory.create_custom_film([:bad], {:name => "new"}).new_record?).must_equal(false) -# expect(Factory.create_custom_film(1)[0].new_record?).must_equal(false) -# expect(Factory.create_custom_film(1, name: "asd")[0].new_record?).must_equal(false) -# expect(Factory.create_custom_film(1, [:bad])[0].new_record?).must_equal(false) -# expect(Factory.create_custom_film(1, {:name => "asd"})[0].new_record?).must_equal(false) -# expect(Factory.create_custom_film(1, [:bad], name: "asd")[0].new_record?).must_equal(false) -# expect(Factory.create_custom_film(1, [:bad], {:name => "asd"})[0].new_record?).must_equal(false) -# end -# end - -# describe "%before_create" do -# it "calls before create" do -# film = CustomFilmFactory.create -# expect(film.name).must_match(/before/) -# film.reload -# expect(film.name).must_match(/before/) -# end -# end - -# describe "%after_create" do -# it "calls callback after creating" do -# film = CustomFilmFactory.create -# expect(film.name).must_match(/after$/) -# film.reload -# expect(film.name).wont_match(/after$/) -# end -# end - -# describe ".create" do -# it "accepts hash attributes" do -# film = FilmFactory.create({:name => "Custom"}) -# expect(film.name).must_equal("Custom") -# expect(film.new_record?).must_equal(false) -# end - -# it "accepts traits" do -# film = FilmFactory.create([:hit, :bad]) -# expect(film.rating).must_equal(0) -# expect(film.name).must_match(/Best Film/) -# expect(film.new_record?).must_equal(false) -# end - -# it "accepts traits and attributes" do -# film = FilmFactory.create([:hit, :bad], {:budget => 10.0f32}) -# expect(film.rating).must_equal(0) -# expect(film.name).must_match(/Best Film/) -# expect(film.budget).must_equal(10.0f32) -# expect(film.new_record?).must_equal(false) -# end - -# it "all model callbacks during creating" do -# film = FilmFactory.create -# expect(film.before_create).must_equal(true) -# expect(film.before_save).must_equal(true) -# expect(film.after_initialize).must_equal(true) -# end - -# describe "ancestor factory" do -# it "accepts no arguments" do -# film = CustomFilmFactory.create -# expect(film.name).must_match(/Custom Film \d*/) -# expect(film.new_record?).must_equal(false) -# end - -# it "accepts hash attributes" do -# film = CustomFilmFactory.create({:name => "Custom"}) -# expect(film.name).must_equal("Custombeforeafter") -# expect(film.new_record?).must_equal(false) -# end - -# it "accepts traits" do -# film = CustomFilmFactory.create([:hit, :bad]) -# expect(film.rating).must_equal(0) -# expect(film.name).must_match(/Best Film/) -# expect(film.new_record?).must_equal(false) -# end - -# it "accepts traits and attributes" do -# film = CustomFilmFactory.create([:hit, :bad], {:budget => 10.0f32}) -# expect(film.rating).must_equal(0) -# expect(film.name).must_match(/Best Film/) -# expect(film.budget).must_equal(10.0f32) -# expect(film.new_record?).must_equal(false) -# end -# end -# end -# end +describe Factory::Jennifer::Base do + let(:described_class) { Factory::Jennifer::Base } + + before do + ::Jennifer::Adapter.adapter.begin_transaction + end + + after do + ::Jennifer::Adapter.adapter.rollback_transaction + end + + describe "%association" do + it "uses factory's defined association" do + film = Factory.create_custom_film([:bad, :hit]) + expect(film.author.nil?).wont_equal(true) + expect(film.author!.name).must_match(/Author \d*$/) + end + + it "uses trait's author if it is given" do + film = Factory.create_fiction_film([:with_special_author]) + expect(film.author.nil?).wont_equal(true) + expect(film.author!.name).must_equal("Special Author") + end + + it "uses given overrides for factory" do + film = Factory.create_fiction_film([:with_special_author]) + expect(film.author!.name).must_equal("Special Author") + end + + it "uses parent association if current factory has no" do + film = Factory.create_fiction_film + expect(film.author.nil?).must_equal(false) + end + + it "creates object without association if there is no one" do + film = Factory.create_film + expect(film.author.nil?).must_equal(true) + end + end + + describe "%factory_creators" do + it "defines all create methods on module level" do + expect(Factory.create_custom_film.new_record?).must_equal(false) + expect(Factory.create_custom_film(name: "New film").new_record?).must_equal(false) + expect(Factory.create_custom_film({:name => "New"}).new_record?).must_equal(false) + expect(Factory.create_custom_film([:bad]).new_record?).must_equal(false) + expect(Factory.create_custom_film([:bad], name: "new").new_record?).must_equal(false) + expect(Factory.create_custom_film([:bad], {:name => "new"}).new_record?).must_equal(false) + expect(Factory.create_custom_film(1)[0].new_record?).must_equal(false) + expect(Factory.create_custom_film(1, name: "asd")[0].new_record?).must_equal(false) + expect(Factory.create_custom_film(1, [:bad])[0].new_record?).must_equal(false) + expect(Factory.create_custom_film(1, {:name => "asd"})[0].new_record?).must_equal(false) + expect(Factory.create_custom_film(1, [:bad], name: "asd")[0].new_record?).must_equal(false) + expect(Factory.create_custom_film(1, [:bad], {:name => "asd"})[0].new_record?).must_equal(false) + end + end + + describe "%before_create" do + it "calls before create" do + film = CustomFilmFactory.create + expect(film.name).must_match(/before/) + film.reload + expect(film.name).must_match(/before/) + end + end + + describe "%after_create" do + it "calls callback after creating" do + film = CustomFilmFactory.create + expect(film.name).must_match(/after$/) + film.reload + expect(film.name).wont_match(/after$/) + end + end + + describe ".create" do + it "accepts hash attributes" do + film = FilmFactory.create({:name => "Custom"}) + expect(film.name).must_equal("Custom") + expect(film.new_record?).must_equal(false) + end + + it "accepts traits" do + film = FilmFactory.create([:hit, :bad]) + expect(film.rating).must_equal(0) + expect(film.name).must_match(/Best Film/) + expect(film.new_record?).must_equal(false) + end + + it "accepts traits and attributes" do + film = FilmFactory.create([:hit, :bad], {:budget => 10.0f32}) + expect(film.rating).must_equal(0) + expect(film.name).must_match(/Best Film/) + expect(film.budget).must_equal(10.0f32) + expect(film.new_record?).must_equal(false) + end + + it "all model callbacks during creating" do + film = FilmFactory.create + expect(film.before_create).must_equal(true) + expect(film.before_save).must_equal(true) + expect(film.after_initialize).must_equal(true) + end + + describe "ancestor factory" do + it "accepts no arguments" do + film = CustomFilmFactory.create + expect(film.name).must_match(/Custom Film \d*/) + expect(film.new_record?).must_equal(false) + end + + it "accepts hash attributes" do + film = CustomFilmFactory.create({:name => "Custom"}) + expect(film.name).must_equal("Custombeforeafter") + expect(film.new_record?).must_equal(false) + end + + it "accepts traits" do + film = CustomFilmFactory.create([:hit, :bad]) + expect(film.rating).must_equal(0) + expect(film.name).must_match(/Best Film/) + expect(film.new_record?).must_equal(false) + end + + it "accepts traits and attributes" do + film = CustomFilmFactory.create([:hit, :bad], {:budget => 10.0f32}) + expect(film.rating).must_equal(0) + expect(film.name).must_match(/Best Film/) + expect(film.budget).must_equal(10.0f32) + expect(film.new_record?).must_equal(false) + end + end + end +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 5c4fa0e..48c0f77 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -1,6 +1,6 @@ -# require "./support/config" -require "minitest/autorun" +require "./support/config" require "../src/factory" -# require "../src/factory/jennifer" -# require "./support/models" +require "../src/factory/jennifer" +require "./support/models" require "./support/factories" +require "minitest/autorun" diff --git a/spec/support/config.cr b/spec/support/config.cr index b932476..1e8fafb 100644 --- a/spec/support/config.cr +++ b/spec/support/config.cr @@ -1,5 +1,5 @@ -require "jennifer/adapter/postgres" require "jennifer" +require "jennifer/adapter/postgres" ::Jennifer::Config.configure do |conf| conf.logger.level = Logger::ERROR diff --git a/spec/support/factories.cr b/spec/support/factories.cr index 07773bd..68b0c67 100644 --- a/spec/support/factories.cr +++ b/spec/support/factories.cr @@ -98,41 +98,41 @@ class HumanPetFactory < PetFactory end end -# class FilmFactory < Factory::Jennifer::Base -# attr :name, "Super Film" -# attr :rating, 2 -# attr :budget, 12.3f32 - -# trait :bad do -# assign :rating, 0 -# end - -# trait :hit do -# assign :rating, 10 -# sequence(:name) { |i| "Best Film #{i}" } -# end -# end - -# class CustomFilmFactory < FilmFactory -# sequence(:name) { |i| "Custom Film #{i}" } - -# association :author, AuthorFactory - -# after_create do |obj| -# obj.name = obj.name! + "after" -# end - -# before_create do |obj| -# obj.name = obj.name! + "before" -# end -# end - -# class FictionFilmFactory < CustomFilmFactory -# trait :with_special_author do -# association :author, options: {name: "Special Author"} -# end -# end - -# class AuthorFactory < Factory::Jennifer::Base -# sequence(:name) { |i| "Author #{i}" } -# end +class FilmFactory < Factory::Jennifer::Base + attr :name, "Super Film" + attr :rating, 2 + attr :budget, 12.3f32 + + trait :bad do + assign :rating, 0 + end + + trait :hit do + assign :rating, 10 + sequence(:name) { |i| "Best Film #{i}" } + end +end + +class CustomFilmFactory < FilmFactory + sequence(:name) { |i| "Custom Film #{i}" } + + association :author, AuthorFactory + + after_create do |obj| + obj.name = obj.name! + "after" + end + + before_create do |obj| + obj.name = obj.name! + "before" + end +end + +class FictionFilmFactory < CustomFilmFactory + trait :with_special_author do + association :author, options: {name: "Special Author"} + end +end + +class AuthorFactory < Factory::Jennifer::Base + sequence(:name) { |i| "Author #{i}" } +end diff --git a/spec/support/models.cr b/spec/support/models.cr index 1659d7c..9c071bc 100644 --- a/spec/support/models.cr +++ b/spec/support/models.cr @@ -1,13 +1,13 @@ class Film < Jennifer::Model::Base mapping( - id: {type: Int32, primary: true}, + id: Primary32, name: String?, rating: Int32, budget: Float32?, author_id: Int32? ) - belongs_to :author, Author + belongs_to :author, klass: Author {% for callback in %i(before_save after_initialize before_create) %} getter {{callback.id}} = false @@ -22,7 +22,7 @@ end class Author < Jennifer::Model::Base mapping( - id: {type: Int32, primary: true}, + id: Primary32, name: String? ) has_many :films, Film diff --git a/src/factory.cr b/src/factory.cr index 7f8ac5e..764f28c 100644 --- a/src/factory.cr +++ b/src/factory.cr @@ -1,105 +1,117 @@ module Factory FACTORIES = {} of String => String - macro default_methods + module BaseDSL + macro included + macro inherited + {% verbatim do %} + SEQUENCES = {} of String => String + ATTRIBUTES = {} of String => String + ASSIGNS = {} of String => String + {% end %} + end + end + macro attr(name, value, klass = nil) - \{% if !value.is_a?(ProcLiteral) %} - \{% if klass != nil %} - @@\{{name.id}} : \{{klass}} = \{{value}} - \{% else %} - @@\{{name.id}} = \{{value}} - \{% end %} - \{% ATTRIBUTES[name.id.stringify] = "plain" %} - \{% else %} - \{% ATTRIBUTES[name.id.stringify] = value.id.stringify %} - \{% end %} + {% if !value.is_a?(ProcLiteral) %} + {% if klass != nil %} + @@{{name.id}} : {{klass}} = {{value}} + {% else %} + @@{{name.id}} = {{value}} + {% end %} + {% ATTRIBUTES[name.id.stringify] = "plain" %} + {% else %} + {% ATTRIBUTES[name.id.stringify] = value.id.stringify %} + {% end %} end macro assign(name, value, klass = nil) - \{% if !value.is_a?(ProcLiteral) %} - \{% if klass != nil %} - @@assign_\{{name.id}} : \{{klass}} = \{{value}} - \{% else %} - @@assign_\{{name.id}} = \{{value}} - \{% end %} - \{% ASSIGNS[name.id.stringify] = "plain" %} - \{% else %} - \{% ASSIGNS[name.id.stringify] = value.id.stringify %} - \{% end %} + {% if !value.is_a?(ProcLiteral) %} + {% if klass != nil %} + @@assign_{{name.id}} : {{klass}} = {{value}} + {% else %} + @@assign_{{name.id}} = {{value}} + {% end %} + {% ASSIGNS[name.id.stringify] = "plain" %} + {% else %} + {% ASSIGNS[name.id.stringify] = value.id.stringify %} + {% end %} end macro sequence(name, init = 0, &block) - \{% sclass = (name.id.stringify.camelcase + "Sequence").id %} - \{% ATTRIBUTES[name.id.stringify] = "-> { ->(#{block.args[0]} : Int32) { #{block.body.id} }.call(#{sclass.id}.next) }" %} - class \{{sclass}} < Factory::Sequence - @@start = \{{init}} + {% class_name = (name.id.stringify.camelcase + "Sequence").id %} + {% ATTRIBUTES[name.id.stringify] = "-> { ->(#{block.args[0]} : Int32) { #{block.body.id} }.call(#{class_name.id}.next) }" %} + class {{class_name}} < ::Factory::Sequence + @@start = {{init}} end end end macro build_methods macro factory_builders(factory_name) + {% verbatim do %} module ::Factory - def self.build_\{{factory_name}} - \{{@type}}.build + def self.build_{{factory_name}} + {{@type}}.build end - def self.build_\{{factory_name}}(**attrs) - \{{@type}}.build(**attrs) + def self.build_{{factory_name}}(**attrs) + {{@type}}.build(**attrs) end - def self.build_\{{factory_name}}(attrs : Hash | NamedTuple) - \{{@type}}.build(attrs) + def self.build_{{factory_name}}(attrs : Hash | NamedTuple) + {{@type}}.build(attrs) end - def self.build_\{{factory_name}}(traits : Array) - \{{@type}}.build(traits) + def self.build_{{factory_name}}(traits : Array) + {{@type}}.build(traits) end - def self.build_\{{factory_name}}(traits : Array, **attrs) - \{{@type}}.build(traits, **attrs) + def self.build_{{factory_name}}(traits : Array, **attrs) + {{@type}}.build(traits, **attrs) end - def self.build_\{{factory_name}}(traits : Array, attrs : Hash | NamedTuple) - \{{@type}}.build(traits, attrs) + def self.build_{{factory_name}}(traits : Array, attrs : Hash | NamedTuple) + {{@type}}.build(traits, attrs) end - def self.build_\{{factory_name}}(count : Int32) - arr = [] of \{{CLASS_NAME.last.id}} - count.times { arr << \{{@type}}.build } + def self.build_{{factory_name}}(count : Int32) + arr = [] of {{CLASS_NAME.last.id}} + count.times { arr << {{@type}}.build } arr end - def self.build_\{{factory_name}}(count : Int32, **attrs) - arr = [] of \{{CLASS_NAME.last.id}} - count.times { arr << \{{@type}}.build(**attrs) } + def self.build_{{factory_name}}(count : Int32, **attrs) + arr = [] of {{CLASS_NAME.last.id}} + count.times { arr << {{@type}}.build(**attrs) } arr end - def self.build_\{{factory_name}}(count : Int32, attrs : Hash | NamedTuple) - arr = [] of \{{CLASS_NAME.last.id}} - count.times { arr << \{{@type}}.build(attrs) } + def self.build_{{factory_name}}(count : Int32, attrs : Hash | NamedTuple) + arr = [] of {{CLASS_NAME.last.id}} + count.times { arr << {{@type}}.build(attrs) } arr end - def self.build_\{{factory_name}}(count : Int32, traits : Array) - arr = [] of \{{CLASS_NAME.last.id}} - count.times { arr << \{{@type}}.build(traits) } + def self.build_{{factory_name}}(count : Int32, traits : Array) + arr = [] of {{CLASS_NAME.last.id}} + count.times { arr << {{@type}}.build(traits) } arr end - def self.build_\{{factory_name}}(count : Int32, traits : Array, **attrs) - arr = [] of \{{CLASS_NAME.last.id}} - count.times { arr << \{{@type}}.build(traits, **attrs) } + def self.build_{{factory_name}}(count : Int32, traits : Array, **attrs) + arr = [] of {{CLASS_NAME.last.id}} + count.times { arr << {{@type}}.build(traits, **attrs) } arr end - def self.build_\{{factory_name}}(count : Int32, traits : Array, attrs : Hash | NamedTuple) - arr = [] of \{{CLASS_NAME.last.id}} - count.times { arr << \{{@type}}.build(traits, attrs) } + def self.build_{{factory_name}}(count : Int32, traits : Array, attrs : Hash | NamedTuple) + arr = [] of {{CLASS_NAME.last.id}} + count.times { arr << {{@type}}.build(traits, attrs) } arr end end + {% end %} end end end diff --git a/src/factory/base.cr b/src/factory/base.cr index add7b68..30d113d 100644 --- a/src/factory/base.cr +++ b/src/factory/base.cr @@ -2,6 +2,8 @@ require "./trait" module Factory class Base + include BaseDSL + IS_FACTORY = ["false"] macro after_finished_hook @@ -28,7 +30,6 @@ module Factory {} of String => String end - Factory.default_methods Factory.build_methods macro not_a_factory @@ -78,154 +79,148 @@ module Factory macro inherited IS_FACTORY = ["true"] TRAITS = {} of String => String - SEQUENCES = {} of String => String CLASS_NAME = [] of String ARGUMENT_TYPE = [] of String - ATTRIBUTES = {} of String => String - ASSIGNS = {} of String => String IGNORED_METHODS = [] of String TRAIT_CLASS_DECLARATIONS = [] of String - \{% if @type.superclass.constant("IS_FACTORY")[-1] == "true" %} - \{% for k, v in @type.superclass.constant("ASSIGNS") %} - \{% ASSIGNS[k] = v %} - \{% end %} - - \{% for k, v in @type.superclass.constant("ATTRIBUTES") %} - \{% ATTRIBUTES[k] = v %} - \{% end %} - \{% end %} - - def self.described_class - \{% begin %} - \{{CLASS_NAME.last.id}} - \{% end %} - end + {% verbatim do %} + {% if @type.superclass.constant("IS_FACTORY")[-1] == "true" %} + {% for k, v in @type.superclass.constant("ASSIGNS") %} {% ASSIGNS[k] = v %} {% end %} + {% for k, v in @type.superclass.constant("ATTRIBUTES") %} {% ATTRIBUTES[k] = v %} {% end %} + {% end %} - macro skip_hash_constructor - \{% IGNORED_METHODS << "hash_constructor" %} - end - - macro skip_empty_constructor - \{% IGNORED_METHODS << "empty_constructor" %} - end + def self.described_class + {% begin %} + {{CLASS_NAME.last.id}} + {% end %} + end - macro finished - \{% for trait in TRAIT_CLASS_DECLARATIONS %} - \{{trait.id}} - \{% end %} + macro skip_hash_constructor + {% IGNORED_METHODS << "hash_constructor" %} + end - def self.get_trait(name : String, go_deep : Bool = true) - \{% for k, v in TRAITS %} - return \{{v.id}} if \{{k}} == name - \{% end %} - super if go_deep + macro skip_empty_constructor + {% IGNORED_METHODS << "empty_constructor" %} end - \{% if ARGUMENT_TYPE.size == 0 && @type.superclass.constant("IS_FACTORY")[-1] == "true" && @type.superclass.constant("ARGUMENT_TYPE").size != 0 %} - \{% ARGUMENT_TYPE.push(@type.superclass.constant("ARGUMENT_TYPE").last) %} - \{% end %} - \{% if CLASS_NAME.empty? %} - \{% if @type.superclass.constant("IS_FACTORY")[-1] == "true" %} - \{% CLASS_NAME.push(@type.superclass.constant("CLASS_NAME").last) %} - \{% else %} - \{% CLASS_NAME.push(@type.stringify.gsub(/Factory$/, "").id) %} - \{% end %} - \{% end %} - - \{% if IS_FACTORY[-1] == "true" %} - \{% factory_name = @type.stringify.gsub(/Factory$/, "").underscore.gsub(/::/, "_") %} - factory_builders(\{{factory_name.id}}) - - \{% Factory::FACTORIES[factory_name] = @type.stringify %} - - \{% if ATTRIBUTES.empty? %} - \{% if !IGNORED_METHODS.includes?("empty_constructor") %} - def self._initialize_with(hash, traits) - obj = described_class.new - make_assigns(obj, traits) - obj - end - \{% end %} - \{% else %} - \{% if !IGNORED_METHODS.includes?("hash_constructor") %} - def self._initialize_with(hash, traits) - obj = described_class.new(hash) - make_assigns(obj, traits) - obj - end - \{% end %} - \{% end %} + macro finished + {% for trait in TRAIT_CLASS_DECLARATIONS %} + {{trait.id}} + {% end %} - def self.build(traits = [] of String | Symbol, **attrs) - obj = _initialize_with(build_attributes(attrs, traits), traits) - _after_initialize(obj) - obj + def self.get_trait(name : String, go_deep : Bool = true) + {% for k, v in TRAITS %} + return {{v.id}} if {{k}} == name + {% end %} + super if go_deep end - def self.build(attrs : Hash | NamedTuple) - obj = _initialize_with(build_attributes(attrs), [] of String) - _after_initialize(obj) - obj - end + {% if ARGUMENT_TYPE.size == 0 && @type.superclass.constant("IS_FACTORY")[-1] == "true" && @type.superclass.constant("ARGUMENT_TYPE").size != 0 %} + {% ARGUMENT_TYPE.push(@type.superclass.constant("ARGUMENT_TYPE").last) %} + {% end %} + {% if CLASS_NAME.empty? %} + {% if @type.superclass.constant("IS_FACTORY")[-1] == "true" %} + {% CLASS_NAME.push(@type.superclass.constant("CLASS_NAME").last) %} + {% else %} + {% CLASS_NAME.push(@type.stringify.gsub(/Factory$/, "").id) %} + {% end %} + {% end %} + + {% if IS_FACTORY[-1] == "true" %} + {% factory_name = @type.stringify.gsub(/Factory$/, "").underscore.gsub(/::/, "_") %} + factory_builders({{factory_name.id}}) + + {% Factory::FACTORIES[factory_name] = @type.stringify %} + + {% if ATTRIBUTES.empty? %} + {% if !IGNORED_METHODS.includes?("empty_constructor") %} + def self._initialize_with(hash, traits) + obj = described_class.new + make_assigns(obj, traits) + obj + end + {% end %} + {% else %} + {% if !IGNORED_METHODS.includes?("hash_constructor") %} + def self._initialize_with(hash, traits) + obj = described_class.new(hash) + make_assigns(obj, traits) + obj + end + {% end %} + {% end %} + + def self.build(traits = [] of String | Symbol, **attrs) + obj = _initialize_with(build_attributes(attrs, traits), traits) + _after_initialize(obj) + obj + end - def self.build(traits : Array, attrs : Hash | NamedTuple) - obj = _initialize_with(build_attributes(attrs, traits), traits) - _after_initialize(obj) - obj - end + def self.build(attrs : Hash | NamedTuple) + obj = _initialize_with(build_attributes(attrs), [] of String) + _after_initialize(obj) + obj + end - def self.attributes - \{% begin %} - \{% if !ATTRIBUTES.empty? %} - \{% if ARGUMENT_TYPE.empty? %} - { - \{% for k, v in ATTRIBUTES %} - \{{k.id.stringify}} => \{% if v =~ /->/ %} \{{v.id}}.call \{% else %} @@\{{k.id}} \{% end %}, - \{% end %} - } - \{% else %} - hash = {} of String => \{{ARGUMENT_TYPE.last.id}} - \{% for k, v in ATTRIBUTES %} - hash[\{{k.id.stringify}}] = \{% if v =~ /->/ %} \{{v.id}}.call \{% else %} @@\{{k.id}} \{% end %} - \{% end %} - hash - \{% end %} - \{% else %} - {} of String => String - \{% end %} - \{% end %} - end + def self.build(traits : Array, attrs : Hash | NamedTuple) + obj = _initialize_with(build_attributes(attrs, traits), traits) + _after_initialize(obj) + obj + end - def self.build_attributes(opts, traits : Array = [] of String | Symbol) - attrs = attributes - traits.each do |name| - trait = get_trait(name.to_s) - raise "Unknown trait \"#{name.to_s}\"" if trait.nil? - trait.not_nil!.add_attributes(attrs) + def self.attributes + {% begin %} + {% if !ATTRIBUTES.empty? %} + {% if ARGUMENT_TYPE.empty? %} + { + {% for k, v in ATTRIBUTES %} + {{k.id.stringify}} => {% if v =~ /->/ %} {{v.id}}.call {% else %} @@{{k.id}} {% end %}, + {% end %} + } + {% else %} + hash = {} of String => {{ARGUMENT_TYPE.last.id}} + {% for k, v in ATTRIBUTES %} + hash[{{k.id.stringify}}] = {% if v =~ /->/ %} {{v.id}}.call {% else %} @@{{k.id}} {% end %} + {% end %} + hash + {% end %} + {% else %} + {} of String => String + {% end %} + {% end %} end - opts.each do |k, v| - attrs[k.to_s] = v + + def self.build_attributes(opts, traits : Array = [] of String | Symbol) + attrs = attributes + traits.each do |name| + trait = get_trait(name.to_s) + raise "Unknown trait \"#{name.to_s}\"" if trait.nil? + trait.not_nil!.add_attributes(attrs) + end + opts.each do |k, v| + attrs[k.to_s] = v + end + attrs end - attrs - end - def self.make_assigns(obj, traits) - \{% begin %} - \{% for k, v in ASSIGNS %} - obj.\{{k.id}} = \{% if v =~ /->/ %} \{{v.id}}.call \{% else %} @@assign_\{{k.id}} \{% end %} - \{% end %} - traits.each do |name| - trait = get_trait(name.to_s) - raise "Unknown trait" if trait.nil? - trait.not_nil!.make_assignes(obj) + def self.make_assigns(obj, traits) + {% begin %} + {% for k, v in ASSIGNS %} + obj.{{k.id}} = {% if v =~ /->/ %} {{v.id}}.call {% else %} @@assign_{{k.id}} {% end %} + {% end %} + traits.each do |name| + trait = get_trait(name.to_s) + raise "Unknown trait" if trait.nil? + trait.not_nil!.make_assigns(obj) + end + {% end %} end - \{% end %} - end - \{% end %} + {% end %} - after_finished_hook - end + after_finished_hook + end + {% end %} end end end diff --git a/src/factory/jennifer/association.cr b/src/factory/jennifer/association.cr new file mode 100644 index 0000000..b287473 --- /dev/null +++ b/src/factory/jennifer/association.cr @@ -0,0 +1,26 @@ +module Factory + module Jennifer + module AssociationDefinition + macro association(name, factory = nil, strategy = :create, options = nil) + {% ASSOCIATIONS << name.id.stringify %} + + {% if factory %} + {% klass = factory %} + {% else %} + {% klass = (name.id.stringify.camelcase + "Factory" ).id %} + {% end %} + + def self.__process_association_{{name.id}}(obj) + association = {{klass}}.build({% if options %}{{options}} {% end %}) + {% if strategy.id.stringify == "build" %} + obj.append_{{name.id}}(association) + {% elsif strategy.id.stringify == "create" %} + obj.add_{{name.id}}(association) + {% else %} + {% raise "Strategy #{strategy.id} of #{@type} is not valid" %} + {% end %} + end + end + end + end +end diff --git a/src/factory/jennifer/base.cr b/src/factory/jennifer/base.cr index 8590a89..55bdb22 100644 --- a/src/factory/jennifer/base.cr +++ b/src/factory/jennifer/base.cr @@ -1,31 +1,10 @@ +require "./association" + module Factory module Jennifer - macro association_macro - macro association(name, factory = nil, strategy = :create, options = nil) - {% verbatim do %} - {% ASSOCIATIONS << name.id.stringify %} - - {% if factory %} - {% klass = factory %} - {% else %} - {% klass = (name.id.stringify.camelcase + "Factory" ).id%} - {% end %} - - def self.__process_association_\{{name.id}}(obj) - aobj = {{klass}}.build({% if options %}{{options}} {% end %}) - {% if strategy.id.stringify == "build" %} - obj.append_{{name.id}}(aobj) - {% elsif strategy.id.stringify == "create" %} - obj.add_{{name.id}}(aobj) - {% else %} - {% raise "Strategy #{strategy.id} of #{@type} is not valid" %} - {% end %} - end - {% end %} - end - end - class Base < ::Factory::Base + include AssociationDefinition + not_a_factory def self._after_create(obj) @@ -51,101 +30,112 @@ module Factory end end + macro trait(name) + {% trait_name = "#{name.id.stringify.camelcase.id}Trait" %} + {% TRAITS[name.id.stringify] = trait_name %} + + macro _macro_trait_{{name.id}} + class {{trait_name.id}} < ::Factory::Trait(\{{CLASS_NAME[-1].id}}) + {{yield.stringify.id}} + end + end + + {% TRAIT_CLASS_DECLARATIONS << "_macro_trait_#{name.id}" %} + end + macro inherited ASSOCIATIONS = [] of String - - ::Factory::Jennifer.association_macro end macro after_finished_hook {% if @type.superclass != ::Factory::Jennifer::Base && @type != ::Factory::Jennifer::Base %} - {% for assoc in @type.superclass.constant("ASSOCIATIONS") %} - {% ASSOCIATIONS << assoc %} - {% end %} + {% for assoc in @type.superclass.constant("ASSOCIATIONS") %} {% ASSOCIATIONS << assoc %} {% end %} {% end %} {% if IS_FACTORY[-1] == "true" %} {% factory_name = @type.stringify.gsub(/Factory$/, "").underscore %} factory_creators({{factory_name.id}}) - \{% if ATTRIBUTES.empty? %} - \{% if !IGNORED_METHODS.includes?("empty_constructor") %} - def self._initialize_with(hash, traits) - obj = described_class.build - make_assigns(obj, traits) - obj - end - \{% end %} - \{% else %} - \{% if !IGNORED_METHODS.includes?("hash_constructor") %} - def self._initialize_with(hash, traits) - obj = described_class.build(hash) - make_assigns(obj, traits) - obj - end - \{% end %} - \{% end %} - - def self.create(traits = [] of String | Symbol, **attrs) - obj = _initialize_with(build_attributes(attrs, traits), traits) - _after_initialize(obj) - _before_create(obj) - obj.save - _after_create(obj) - obj - end + {% verbatim do %} + {% if ATTRIBUTES.empty? %} + {% if !IGNORED_METHODS.includes?("empty_constructor") %} + def self._initialize_with(hash, traits) + obj = described_class.build + make_assigns(obj, traits) + obj + end + {% end %} + {% else %} + {% if !IGNORED_METHODS.includes?("hash_constructor") %} + def self._initialize_with(hash, traits) + obj = described_class.build(hash) + make_assigns(obj, traits) + obj + end + {% end %} + {% end %} - def self.create(attrs : Hash) - obj = _initialize_with(build_attributes(attrs), [] of String) - _after_initialize(obj) - _before_create(obj) - obj.save - _after_create(obj) - obj - end + def self.create(traits = [] of String | Symbol, **attrs) + obj = _initialize_with(build_attributes(attrs, traits), traits) + _after_initialize(obj) + _before_create(obj) + obj.save + _after_create(obj) + obj + end - def self.create(traits : Array, attrs : Hash) - obj = _initialize_with(build_attributes(attrs, traits), traits) - _after_initialize(obj) - _before_create(obj) - obj.save - _after_create(obj) - obj - end + def self.create(attrs : Hash) + obj = _initialize_with(build_attributes(attrs), [] of String) + _after_initialize(obj) + _before_create(obj) + obj.save + _after_create(obj) + obj + end - def self.make_assigns(obj, traits : Array) - \{% for k, v in ASSIGNS %} - obj.\{{k.id}} = \{% if v =~ /->/ %} \{{v.id}}.call \{% else %} @@assign_\{{k.id}} \{% end %} - \{% end %} - traits.each do |name| - trait = get_trait(name.to_s) - raise "Unknown trait" if trait.nil? - trait.not_nil!.make_assignes(obj) + def self.create(traits : Array, attrs : Hash) + obj = _initialize_with(build_attributes(attrs, traits), traits) + _after_initialize(obj) + _before_create(obj) + obj.save + _after_create(obj) + obj end - add_associations(obj, traits) - end - def self.add_associations(obj, traits : Array) - \{% if !ASSOCIATIONS.empty? %} - trait_classes = traits.map { |e| get_trait(e) }.compact - \{% for assoc in ASSOCIATIONS %} - process_association(obj, trait_classes, \{{assoc}}) - \{% end %} - \{% end %} - end + def self.make_assigns(obj, traits : Array) + {% for k, v in ASSIGNS %} + obj.{{k.id}} = {% if v =~ /->/ %} {{v.id}}.call {% else %} @@assign_{{k.id}} {% end %} + {% end %} + traits.each do |name| + trait = get_trait(name.to_s) + raise "Unknown trait" if trait.nil? + trait.not_nil!.make_assigns(obj) + end + add_associations(obj, traits) + end - def self.process_association(obj, trait_classes : Array, assoc) - trait_classes.each do |t| - if t.associations.includes?(assoc) - t.process_association(obj, assoc) - return + def self.add_associations(obj, traits : Array) + {% if !ASSOCIATIONS.empty? %} + trait_classes = traits.map { |e| get_trait(e) }.compact + {% for assoc in ASSOCIATIONS %} + process_association(obj, trait_classes, {{assoc}}) + {% end %} + {% end %} + end + + def self.process_association(obj, trait_classes : Array, assoc) + trait_classes.each do |t| + if t.associations.includes?(assoc) + t.process_association(obj, assoc) + return + end end + {% for assoc in ASSOCIATIONS %} + __process_association_{{assoc.id}}(obj) if {{assoc}} == assoc + {% end %} + super(obj, trait_classes[0...1], assoc) end - \{% for assoc in ASSOCIATIONS %} - __process_association_\{{assoc.id}}(obj) if \{{assoc}} == assoc - \{% end %} - super(obj, trait_classes[0...1], assoc) - end + {% end %} {% end %} end diff --git a/src/factory/jennifer/trait.cr b/src/factory/jennifer/trait.cr index a9b7a95..1264ac0 100644 --- a/src/factory/jennifer/trait.cr +++ b/src/factory/jennifer/trait.cr @@ -1,19 +1,23 @@ +require "./association" + module Factory class Trait(T) + include Jennifer::AssociationDefinition + macro inherited + {% verbatim do %} ASSOCIATIONS = [] of String - ::Factory::Jennifer.association_macro - def self.associations ASSOCIATIONS end def self.process_association(obj, assoc : String) - \{% for assoc in ASSOCIATIONS %} - __process_association_\{{assoc.id}}(obj) if assoc == \{{assoc}} - \{% end %} + {% for assoc in ASSOCIATIONS %} + __process_association_{{assoc.id}}(obj) if assoc == {{assoc}} + {% end %} end + {% end %} end end end diff --git a/src/factory/trait.cr b/src/factory/trait.cr index 21ce493..7ef581f 100644 --- a/src/factory/trait.cr +++ b/src/factory/trait.cr @@ -1,11 +1,11 @@ module Factory abstract class Trait(T) - Factory.default_methods + include BaseDSL def self.add_attributes(hash) : Void end - def self.make_assignes(obj : T) : Void + def self.make_assigns(obj : T) : Void end def self.process_association(obj, assoc) @@ -16,30 +16,29 @@ module Factory end macro inherited - SEQUENCES = {} of String => String CLASS_NAME = [] of String HASH_TYPE = [] of String - ATTRIBUTES = {} of String => String - ASSIGNS = {} of String => String macro finished - def self.add_attributes(hash) : Void - \{% if !ATTRIBUTES.empty? %} - { - \{% for k, v in ATTRIBUTES %} - \{{k.id.stringify}} => \{% if v =~ /->/ %} \{{v.id}}.call \{% else %} @@\{{k.id}} \{% end %}, - \{% end %} - }.each do |k, v| - hash[k] = v - end - \{% end %} - end + {% verbatim do %} + def self.add_attributes(hash) : Void + {% if !ATTRIBUTES.empty? %} + { + {% for k, v in ATTRIBUTES %} + {{k.id.stringify}} => {% if v =~ /->/ %} {{v.id}}.call {% else %} @@{{k.id}} {% end %}, + {% end %} + }.each do |k, v| + hash[k] = v + end + {% end %} + end - def self.make_assignes(obj : T) : Void - \{% for k, v in ASSIGNS %} - obj.\{{k.id}} = \{% if v =~ /->/ %} \{{v.id}}.call \{% else %} @@assign_\{{k.id}} \{% end %} - \{% end %} - end + def self.make_assigns(obj : T) : Void + {% for k, v in ASSIGNS %} + obj.{{k.id}} = {% if v =~ /->/ %} {{v.id}}.call {% else %} @@assign_{{k.id}} {% end %} + {% end %} + end + {% end %} end end end diff --git a/src/factory/version.cr b/src/factory/version.cr index 3d633a0..1f206f2 100644 --- a/src/factory/version.cr +++ b/src/factory/version.cr @@ -1,3 +1,3 @@ module Factory - VERSION = "0.1.2" + VERSION = "0.1.4" end