diff --git a/README.md b/README.md index 2eb1407..f87e08c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Vorpal [![Build Status](https://travis-ci.com/nulogy/vorpal.svg?branch=master)](https://travis-ci.com/nulogy/vorpal) [![Code Climate](https://codeclimate.com/github/nulogy/vorpal/badges/gpa.svg)](https://codeclimate.com/github/nulogy/vorpal) [![Code Coverage](https://codecov.io/gh/nulogy/vorpal/branch/master/graph/badge.svg)](https://codecov.io/gh/nulogy/vorpal/branch/master) +# Vorpal [![Build Status](https://travis-ci.com/nulogy/vorpal.svg?branch=main)](https://travis-ci.com/nulogy/vorpal) [![Code Climate](https://codeclimate.com/github/nulogy/vorpal/badges/gpa.svg)](https://codeclimate.com/github/nulogy/vorpal) [![Code Coverage](https://codecov.io/gh/nulogy/vorpal/branch/main/graph/badge.svg)](https://codecov.io/gh/nulogy/vorpal/branch/main) Separate your domain model from your persistence mechanism. Some problems call for a really sharp tool. @@ -156,9 +156,35 @@ TreeRepository.destroy(dead_tree) TreeRepository.destroy_by_id(dead_tree_id) ``` +### Ids + +Vorpal by default will use auto-incrementing Integers from a DB sequence for ids. However, UUID v4 ids are also +supported: + +```ruby +Vorpal.define do + # UUID v4 id! + map Tree, primary_key_type: :uuid do + # .. + end + + # Also a UUID v4 id, the Rails Way! + map Trunk, id: :uuid do + # .. + end + + # If you feel the need to specify an auto-incrementing integer id. + map Branch, primary_key_type: :serial do + # .. + end +end +``` + +CAVEAT: Vorpal currently does NOT SUPPORT anyone but Vorpal setting the id of an entity! + ## API Documentation -http://rubydoc.info/github/nulogy/vorpal/master/frames +http://rubydoc.info/github/nulogy/vorpal/main/frames ## Caveats It also does not do some things that you might expect from other ORMs: @@ -175,9 +201,8 @@ It also does not do some things that you might expect from other ORMs: 1. Only supports PostgreSQL. ## Future Enhancements -* Support for UUID primary keys. +* Support for clients to set UUID-based ids. * Nicer DSL for specifying attributes that have different names in the domain model than in the DB. -* Show how to implement POROs without using Virtus (it is unsupported and can be crazy slow) * Aggregate updated_at. * Better support for value objects. @@ -195,7 +220,7 @@ It also does not do some things that you might expect from other ORMs: **A.** Create a method on a [Repository](http://martinfowler.com/eaaCatalog/repository.html)! They have full access to the DB/ORM so you can use [Arel](https://github.com/rails/arel) and go [crazy](http://asciicasts.com/episodes/239-activerecord-relation-walkthrough) or use direct SQL if you want. -For example, use the [#query](https://rubydoc.info/github/nulogy/vorpal/master/Vorpal/AggregateMapper#query-instance_method) method on the [AggregateMapper](https://rubydoc.info/github/nulogy/vorpal/master/Vorpal/AggregateMapper) to access the underyling [ActiveRecordRelation](https://api.rubyonrails.org/classes/ActiveRecord/Relation.html): +For example, use the [#query](https://rubydoc.info/github/nulogy/vorpal/main/Vorpal/AggregateMapper#query-instance_method) method on the [AggregateMapper](https://rubydoc.info/github/nulogy/vorpal/main/Vorpal/AggregateMapper) to access the underyling [ActiveRecordRelation](https://api.rubyonrails.org/classes/ActiveRecord/Relation.html): ```ruby def find_special_ones diff --git a/lib/vorpal/config/class_config.rb b/lib/vorpal/config/class_config.rb new file mode 100644 index 0000000..fe9d539 --- /dev/null +++ b/lib/vorpal/config/class_config.rb @@ -0,0 +1,71 @@ +require 'equalizer' + +module Vorpal + module Config + # @private + class ClassConfig + include Equalizer.new(:domain_class, :db_class) + attr_reader :serializer, :deserializer, :domain_class, :db_class, :primary_key_type, :local_association_configs + attr_accessor :has_manys, :belongs_tos, :has_ones + + ALLOWED_PRIMARY_KEY_TYPE_OPTIONS = [:serial, :uuid] + + def initialize(attrs) + @has_manys = [] + @belongs_tos = [] + @has_ones = [] + @local_association_configs = [] + + @serializer = attrs[:serializer] + @deserializer = attrs[:deserializer] + @domain_class = attrs[:domain_class] + @db_class = attrs[:db_class] + @primary_key_type = attrs[:primary_key_type] + raise "Invalid primary_key_type: '#{@primary_key_type}'" unless ALLOWED_PRIMARY_KEY_TYPE_OPTIONS.include?(@primary_key_type) + end + + def build_db_object(attributes) + db_class.new(attributes) + end + + def set_db_object_attributes(db_object, attributes) + db_object.attributes = attributes + end + + def get_db_object_attributes(db_object) + symbolize_keys(db_object.attributes) + end + + def serialization_required? + domain_class.superclass.name != 'ActiveRecord::Base' + end + + def serialize(object) + serializer.serialize(object) + end + + def deserialize(db_object) + attributes = get_db_object_attributes(db_object) + serialization_required? ? deserializer.deserialize(domain_class.new, attributes) : db_object + end + + def set_attribute(db_object, attribute, value) + db_object.send("#{attribute}=", value) + end + + def get_attribute(db_object, attribute) + db_object.send(attribute) + end + + private + + def symbolize_keys(hash) + result = {} + hash.each_key do |key| + result[key.to_sym] = hash[key] + end + result + end + end + end +end diff --git a/lib/vorpal/configs.rb b/lib/vorpal/configs.rb index 63daefa..1395677 100644 --- a/lib/vorpal/configs.rb +++ b/lib/vorpal/configs.rb @@ -1,13 +1,13 @@ require 'vorpal/util/hash_initialization' require 'vorpal/exceptions' +require 'vorpal/config/class_config' require 'equalizer' module Vorpal # @private - class MasterConfig - def initialize(class_configs) - @class_configs = class_configs - initialize_association_configs + class MainConfig + def initialize + @class_configs = [] end def config_for(clazz) @@ -20,7 +20,9 @@ def config_for_db_object(db_object) @class_configs.detect { |conf| conf.db_class == db_object.class } end - private + def add_class_config(class_config) + @class_configs << class_config + end def initialize_association_configs association_configs = {} @@ -50,6 +52,8 @@ def initialize_association_configs end end + private + def build_association_config(association_configs, local_config, fk, fk_type) association_config = AssociationConfig.new(local_config, fk, fk_type) if association_configs[association_config] @@ -126,67 +130,6 @@ def foreign_key_info(remote_class_config) end end - # @private - class ClassConfig - include Equalizer.new(:domain_class, :db_class) - attr_reader :serializer, :deserializer, :domain_class, :db_class, :local_association_configs - attr_accessor :has_manys, :belongs_tos, :has_ones - - def initialize(attrs) - @has_manys = [] - @belongs_tos = [] - @has_ones = [] - @local_association_configs = [] - - attrs.each do |k,v| - instance_variable_set("@#{k}", v) - end - end - - def build_db_object(attributes) - db_class.new(attributes) - end - - def set_db_object_attributes(db_object, attributes) - db_object.attributes = attributes - end - - def get_db_object_attributes(db_object) - symbolize_keys(db_object.attributes) - end - - def serialization_required? - domain_class.superclass.name != 'ActiveRecord::Base' - end - - def serialize(object) - serializer.serialize(object) - end - - def deserialize(db_object) - attributes = get_db_object_attributes(db_object) - serialization_required? ? deserializer.deserialize(domain_class.new, attributes) : db_object - end - - def set_attribute(db_object, attribute, value) - db_object.send("#{attribute}=", value) - end - - def get_attribute(db_object, attribute) - db_object.send(attribute) - end - - private - - def symbolize_keys(hash) - result = {} - hash.each_key do |key| - result[key.to_sym] = hash[key] - end - result - end - end - # @private class ForeignKeyInfo include Equalizer.new(:fk_column, :fk_type_column, :fk_type) diff --git a/lib/vorpal/dsl/config_builder.rb b/lib/vorpal/dsl/config_builder.rb index 56915ff..23fc30f 100644 --- a/lib/vorpal/dsl/config_builder.rb +++ b/lib/vorpal/dsl/config_builder.rb @@ -16,74 +16,32 @@ def initialize(clazz, options, db_driver) @defaults_generator = DefaultsGenerator.new(clazz, db_driver) end - # Maps the given attributes to and from the domain object and the DB. Not needed - # if a serializer and deserializer were provided. + # @private def attributes(*attributes) @attributes.concat(attributes) end - # Defines a one-to-many association to another type where the foreign key is stored on the child. - # - # In Object-Oriented programming, associations are *directed*. This means that they can only be - # traversed in one direction: from the type that defines the association (the one with the - # getter) to the type that is associated. They end that defines the association is called the - # 'Parent' and the end that is associated is called the 'Child'. - # - # @param name [String] Name of the association getter. - # @param options [Hash] - # @option options [Boolean] :owned (True) True if the child type belongs to the aggregate. Changes to any object belonging to the aggregate will be persisted when the aggregate is persisted. - # @option options [String] :fk (Parent class name converted to snakecase and appended with a '_id') The name of the DB column on the child that contains the foreign key reference to the parent. - # @option options [String] :fk_type The name of the DB column on the child that contains the parent class name. Only needed when there is an association from the child side that is polymorphic. - # @option options [Class] :child_class (name converted to a Class) The child class. + # @private def has_many(name, options={}) - @has_manys << {name: name}.merge(options) + @has_manys << build_has_many({name: name}.merge(options)) end - # Defines a one-to-one association to another type where the foreign key - # is stored on the child. - # - # In Object-Oriented programming, associations are *directed*. This means that they can only be - # traversed in one direction: from the type that defines the association (the one with the - # getter) to the type that is associated. They end that defines the association is called the - # 'Parent' and the end that is associated is called the 'Child'. - # - # @param name [String] Name of the association getter. - # @param options [Hash] - # @option options [Boolean] :owned (True) True if the child type belongs to the aggregate. Changes to any object belonging to the aggregate will be persisted when the aggregate is persisted. - # @option options [String] :fk (Parent class name converted to snakecase and appended with a '_id') The name of the DB column on the child that contains the foreign key reference to the parent. - # @option options [String] :fk_type The name of the DB column on the child that contains the parent class name. Only needed when there is an association from the child side that is polymorphic. - # @option options [Class] :child_class (name converted to a Class) The child class. + # @private def has_one(name, options={}) - @has_ones << {name: name}.merge(options) + @has_ones << build_has_one({name: name}.merge(options)) end - # Defines a one-to-one association with another type where the foreign key - # is stored on the parent. - # - # This association can be polymorphic. I.E. children can be of different types. - # - # In Object-Oriented programming, associations are *directed*. This means that they can only be - # traversed in one direction: from the type that defines the association (the one with the - # getter) to the type that is associated. They end that defines the association is called the - # 'Parent' and the end that is associated is called the 'Child'. - # - # @param name [String] Name of the association getter. - # @param options [Hash] - # @option options [Boolean] :owned (True) True if the child type belongs to the aggregate. Changes to any object belonging to the aggregate will be persisted when the aggregate is persisted. - # @option options [String] :fk (Child class name converted to snakecase and appended with a '_id') The name of the DB column on the parent that contains the foreign key reference to the child. - # @option options [String] :fk_type The name of the DB column on the parent that contains the child class name. Only needed when the association is polymorphic. - # @option options [Class] :child_class (name converted to a Class) The child class. - # @option options [[Class]] :child_classes The list of possible classes that can be children. This is for polymorphic associations. Takes precedence over `:child_class`. + # @private def belongs_to(name, options={}) - @belongs_tos << {name: name}.merge(options) + @belongs_tos << build_belongs_to({name: name}.merge(options)) end # @private def build class_config = build_class_config - class_config.has_manys = build_has_manys - class_config.has_ones = build_has_ones - class_config.belongs_tos = build_belongs_tos + class_config.has_manys = @has_manys + class_config.has_ones = @has_ones + class_config.belongs_tos = @belongs_tos class_config end @@ -96,18 +54,15 @@ def attributes_with_id private def build_class_config - Vorpal::ClassConfig.new( + Vorpal::Config::ClassConfig.new( domain_class: @domain_class, db_class: @class_options[:to] || @defaults_generator.build_db_class(@class_options[:table_name]), serializer: @class_options[:serializer] || @defaults_generator.serializer(attributes_with_id), deserializer: @class_options[:deserializer] || @defaults_generator.deserializer(attributes_with_id), + primary_key_type: @class_options[:primary_key_type] || @class_options[:id] || :serial, ) end - def build_has_manys - @has_manys.map { |options| build_has_many(options) } - end - def build_has_many(options) options[:child_class] ||= @defaults_generator.child_class(options[:name]) options[:fk] ||= @defaults_generator.foreign_key(@domain_class.name) @@ -115,10 +70,6 @@ def build_has_many(options) Vorpal::HasManyConfig.new(options) end - def build_has_ones - @has_ones.map { |options| build_has_one(options) } - end - def build_has_one(options) options[:child_class] ||= @defaults_generator.child_class(options[:name]) options[:fk] ||= @defaults_generator.foreign_key(@domain_class.name) @@ -126,10 +77,6 @@ def build_has_one(options) Vorpal::HasOneConfig.new(options) end - def build_belongs_tos - @belongs_tos.map { |options| build_belongs_to(options) } - end - def build_belongs_to(options) child_class = options[:child_classes] || options[:child_class] || @defaults_generator.child_class(options[:name]) options[:child_classes] = Array(child_class) diff --git a/lib/vorpal/dsl/configuration.rb b/lib/vorpal/dsl/configuration.rb index 0a76329..44e7928 100644 --- a/lib/vorpal/dsl/configuration.rb +++ b/lib/vorpal/dsl/configuration.rb @@ -4,54 +4,143 @@ module Vorpal module Dsl - module Configuration - - # Configures and creates a {Engine} instance. - # - # @param options [Hash] Global configuration options for the engine instance. - # @option options [Object] :db_driver (Object that will be used to interact with the DB.) - # Must be duck-type compatible with {Postgresql}. + # Implements the Vorpal DSL. # - # @return [Engine] Instance of the mapping engine. - def define(options={}, &block) - master_config = build_config(&block) - db_driver = options.fetch(:db_driver, Driver::Postgresql.new) - Engine.new(db_driver, master_config) - end - - # Maps a domain class to a relational table. + # ```ruby + # engine = Vorpal.define do + # map Tree do + # attributes :name + # belongs_to :trunk + # has_many :branches + # end # - # @param domain_class [Class] Type of the domain model to be mapped - # @param options [Hash] Configure how to map the domain model - # @option options [String] :to - # Class of the ActiveRecord object that will map this domain class to the DB. - # Optional, if one is not specified, it will be generated. - # @option options [Object] :serializer (map the {ConfigBuilder#attributes} directly) - # Object that will convert the domain objects into a hash. + # map Trunk do + # attributes :length + # has_one :tree + # end # - # Must have a `(Hash) serialize(Object)` method. - # @option options [Object] :deserializer (map the {ConfigBuilder#attributes} directly) - # Object that will set a hash of attribute_names->values onto a new domain - # object. + # map Branch do + # attributes :length + # belongs_to :tree + # end + # end # - # Must have a `(Object) deserialize(Object, Hash)` method. - def map(domain_class, options={}, &block) - @class_configs << build_class_config(domain_class, options, &block) - end + # mapper = engine.mapper_for(Tree) + # ``` + module Configuration + # Configures and creates a {Engine} instance. + # + # @param options [Hash] Global configuration options for the engine instance. + # @option options [Object] :db_driver (Object that will be used to interact with the DB.) + # Must be duck-type compatible with {Postgresql}. + # + # @return [Engine] Instance of the mapping engine. + def define(options={}, &block) + @main_config = MainConfig.new + instance_exec(&block) + @main_config.initialize_association_configs + db_driver = options.fetch(:db_driver, Driver::Postgresql.new) + engine = Engine.new(db_driver, @main_config) + @main_config = nil # make sure this MainConfig is never re-used by accident. + engine + end - # @private - def build_class_config(domain_class, options={}, &block) - builder = ConfigBuilder.new(domain_class, options, Driver::Postgresql.new) - builder.instance_exec(&block) if block_given? - builder.build - end + # Maps a domain class to a relational table. + # + # @param domain_class [Class] Type of the domain model to be mapped + # @param options [Hash] Configure how to map the domain model + # @option options [String] :to + # Class of the ActiveRecord object that will map this domain class to the DB. + # Optional, if one is not specified, it will be generated. + # @option options [Object] :serializer (map the {ConfigBuilder#attributes} directly) + # Object that will convert the domain objects into a hash. + # + # Must have a `(Hash) serialize(Object)` method. + # @option options [Object] :deserializer (map the {ConfigBuilder#attributes} directly) + # Object that will set a hash of attribute_names->values onto a new domain + # object. + # + # Must have a `(Object) deserialize(Object, Hash)` method. + # @option options [Symbol] :primary_key_type [:serial, :uuid] (:serial) + # The type of primary key for the class. :serial for auto-incrementing integer, :uuid for a UUID + # @option options [Symbol] :id + # Same as :primary_key_type. Exists for compatibility with the Rails API. + def map(domain_class, options={}, &block) + class_config = build_class_config(domain_class, options, &block) + @main_config.add_class_config(class_config) + class_config + end - # @private - def build_config(&block) - @class_configs = [] - self.instance_exec(&block) - MasterConfig.new(@class_configs) + # @private + def build_class_config(domain_class, options, &block) + @builder = ConfigBuilder.new(domain_class, options, Driver::Postgresql.new) + instance_exec(&block) if block_given? + class_config = @builder.build + @builder = nil # make sure this ConfigBuilder is never re-used by accident. + class_config + end + + # Maps the given attributes to and from the domain object and the DB. Not needed + # if a serializer and deserializer were provided. + def attributes(*attributes) + @builder.attributes(*attributes) + end + + # Defines a one-to-many association to another type where the foreign key is stored on the child. + # + # In Object-Oriented programming, associations are *directed*. This means that they can only be + # traversed in one direction: from the type that defines the association (the one with the + # getter) to the type that is associated. They end that defines the association is called the + # 'Parent' and the end that is associated is called the 'Child'. + # + # @param name [String] Name of the association getter. + # @param options [Hash] + # @option options [Boolean] :owned (True) True if the child type belongs to the aggregate. Changes to any object belonging to the aggregate will be persisted when the aggregate is persisted. + # @option options [String] :fk (Parent class name converted to snakecase and appended with a '_id') The name of the DB column on the child that contains the foreign key reference to the parent. + # @option options [String] :fk_type The name of the DB column on the child that contains the parent class name. Only needed when there is an association from the child side that is polymorphic. + # @option options [Class] :child_class (name converted to a Class) The child class. + def has_many(name, options={}) + @builder.has_many(name, options) + end + + # Defines a one-to-one association to another type where the foreign key + # is stored on the child. + # + # In Object-Oriented programming, associations are *directed*. This means that they can only be + # traversed in one direction: from the type that defines the association (the one with the + # getter) to the type that is associated. They end that defines the association is called the + # 'Parent' and the end that is associated is called the 'Child'. + # + # @param name [String] Name of the association getter. + # @param options [Hash] + # @option options [Boolean] :owned (True) True if the child type belongs to the aggregate. Changes to any object belonging to the aggregate will be persisted when the aggregate is persisted. + # @option options [String] :fk (Parent class name converted to snakecase and appended with a '_id') The name of the DB column on the child that contains the foreign key reference to the parent. + # @option options [String] :fk_type The name of the DB column on the child that contains the parent class name. Only needed when there is an association from the child side that is polymorphic. + # @option options [Class] :child_class (name converted to a Class) The child class. + def has_one(name, options={}) + @builder.has_one(name, options) + end + + # Defines a one-to-one association with another type where the foreign key + # is stored on the parent. + # + # This association can be polymorphic. I.E. children can be of different types. + # + # In Object-Oriented programming, associations are *directed*. This means that they can only be + # traversed in one direction: from the type that defines the association (the one with the + # getter) to the type that is associated. They end that defines the association is called the + # 'Parent' and the end that is associated is called the 'Child'. + # + # @param name [String] Name of the association getter. + # @param options [Hash] + # @option options [Boolean] :owned (True) True if the child type belongs to the aggregate. Changes to any object belonging to the aggregate will be persisted when the aggregate is persisted. + # @option options [String] :fk (Child class name converted to snakecase and appended with a '_id') The name of the DB column on the parent that contains the foreign key reference to the child. + # @option options [String] :fk_type The name of the DB column on the parent that contains the child class name. Only needed when the association is polymorphic. + # @option options [Class] :child_class (name converted to a Class) The child class. + # @option options [[Class]] :child_classes The list of possible classes that can be children. This is for polymorphic associations. Takes precedence over `:child_class`. + def belongs_to(name, options={}) + @builder.belongs_to(name, options) + end end end - end end diff --git a/lib/vorpal/engine.rb b/lib/vorpal/engine.rb index 64a9823..74fdf42 100644 --- a/lib/vorpal/engine.rb +++ b/lib/vorpal/engine.rb @@ -7,9 +7,9 @@ module Vorpal class Engine # @private - def initialize(db_driver, master_config) + def initialize(db_driver, main_config) @db_driver = db_driver - @configs = master_config + @configs = main_config end # Creates a mapper for saving/updating/loading/destroying an aggregate to/from @@ -34,6 +34,9 @@ def persist(roots) serialize(all_owned_objects, mapping, loaded_db_objects) new_objects = get_unsaved_objects(mapping.keys) begin + # Primary keys are set eagerly (instead of waiting for them to be set by ActiveRecord upon create) + # because we want to support non-null FK constraints without needing to figure the correct + # order to save entities in. set_primary_keys(all_owned_objects, mapping) set_foreign_keys(all_owned_objects, mapping) remove_orphans(mapping, loaded_db_objects) @@ -89,10 +92,16 @@ def db_class(domain_class) @configs.config_for(domain_class).db_class end + # Try to use {AggregateMapper#query} instead. def query(domain_class) @db_driver.query(@configs.config_for(domain_class).db_class, mapper_for(domain_class)) end + # @private + def class_config(domain_class) + @configs.config_for(domain_class) + end + private def wrap(collection_or_not) @@ -165,7 +174,11 @@ def serialize_object(object, config, loaded_db_objects) def set_primary_keys(owned_objects, mapping) owned_objects.each do |config, objects| in_need_of_primary_keys = objects.find_all { |obj| obj.id.nil? } - primary_keys = @db_driver.get_primary_keys(config.db_class, in_need_of_primary_keys.length) + if config.primary_key_type == :uuid + primary_keys = Array.new(in_need_of_primary_keys.length) { SecureRandom.uuid } + elsif config.primary_key_type == :serial + primary_keys = @db_driver.get_primary_keys(config.db_class, in_need_of_primary_keys.length) + end in_need_of_primary_keys.zip(primary_keys).each do |object, primary_key| mapping[object].id = primary_key object.id = primary_key diff --git a/spec/acceptance/vorpal/aggregate_mapper_spec.rb b/spec/acceptance/vorpal/aggregate_mapper_spec.rb index 0423c86..d77911d 100644 --- a/spec/acceptance/vorpal/aggregate_mapper_spec.rb +++ b/spec/acceptance/vorpal/aggregate_mapper_spec.rb @@ -739,6 +739,37 @@ def initialize(id: nil, name: "", trunk: nil, environment: nil, fissures: [], br end end + class TreeUUID + attr_accessor :id + attr_accessor :name + + def initialize(id: nil, name: "") + @id = id + @name = name + end + end + + describe 'UUID support' do + before(:all) do + db_connection.enable_extension 'pgcrypto' + define_table('tree_uuids', { name: :text }, true, id: :uuid) + end + + it 'generates a UUID as a PK on create' do + engine = Vorpal.define do + map TreeUUID, id: :uuid do + attributes :name + end + end + mapper = engine.mapper_for(TreeUUID) + + tree = TreeUUID.new(name: 'new tree') + mapper.persist([tree]) + + expect(tree.id).to be_a(String) + end + end + private def db_class_for(clazz, mapper) diff --git a/spec/helpers/db_helpers.rb b/spec/helpers/db_helpers.rb index 15580e4..6f6989d 100644 --- a/spec/helpers/db_helpers.rb +++ b/spec/helpers/db_helpers.rb @@ -39,12 +39,10 @@ def establish_connection end # when you change a table's columns, set force to true to re-generate the table in the DB - def define_table(table_name, columns, force) - if table_name_is_free?(table_name) || force - db_connection.create_table(table_name, force: true) do |t| - columns.each do |name, type| - t.send(type, name) - end + def define_table(table_name, columns, force, create_options = {}) + create_table(table_name, force: force, **create_options) do |t| + columns.each do |name, type| + t.send(type, name) end end end diff --git a/spec/unit/vorpal/config/class_config_spec.rb b/spec/unit/vorpal/config/class_config_spec.rb new file mode 100644 index 0000000..ae5c589 --- /dev/null +++ b/spec/unit/vorpal/config/class_config_spec.rb @@ -0,0 +1,25 @@ +require 'unit_spec_helper' + +require 'vorpal/config/class_config' + +describe Vorpal::Config::ClassConfig do + describe 'primary_key_type options' do + it 'allows :serial' do + class_config = described_class.new(primary_key_type: :serial) + + expect(class_config.primary_key_type).to eq(:serial) + end + + it 'allows :uuid' do + class_config = described_class.new(primary_key_type: :uuid) + + expect(class_config.primary_key_type).to eq(:uuid) + end + + it 'does not allow others' do + expect do + described_class.new(primary_key_type: :invalid) + end.to raise_exception("Invalid primary_key_type: 'invalid'") + end + end +end diff --git a/spec/unit/vorpal/configs_spec.rb b/spec/unit/vorpal/configs_spec.rb index 52a9692..a1b0523 100644 --- a/spec/unit/vorpal/configs_spec.rb +++ b/spec/unit/vorpal/configs_spec.rb @@ -1,7 +1,7 @@ require 'unit_spec_helper' require 'vorpal/configs' -describe Vorpal::MasterConfig do +describe Vorpal::MainConfig do class Post attr_accessor :comments attr_accessor :best_comment @@ -11,8 +11,8 @@ class Comment attr_accessor :post end - let(:post_config) { Vorpal::ClassConfig.new(domain_class: Post) } - let(:comment_config) { Vorpal::ClassConfig.new(domain_class: Comment) } + let(:post_config) { Vorpal::Config::ClassConfig.new(domain_class: Post, primary_key_type: :serial) } + let(:comment_config) { Vorpal::Config::ClassConfig.new(domain_class: Comment, primary_key_type: :serial) } let(:post_has_many_comments_config) { Vorpal::HasManyConfig.new(name: 'comments', fk: 'post_id', child_class: Comment) } let(:post_has_one_comment_config) { Vorpal::HasOneConfig.new(name: 'best_comment', fk: 'post_id', child_class: Comment) } let(:comment_belongs_to_post_config) { Vorpal::BelongsToConfig.new(name: 'post', fk: 'post_id', child_classes: [Post]) } @@ -21,7 +21,7 @@ class Comment it 'builds an association_config for a belongs_to' do comment_config.belongs_tos << comment_belongs_to_post_config - Vorpal::MasterConfig.new([post_config, comment_config]) + initialize_association_configs([post_config, comment_config]) expect(comment_config.local_association_configs.size).to eq(1) expect(post_config.local_association_configs.size).to eq(0) @@ -31,7 +31,7 @@ class Comment comment_config.belongs_tos << comment_belongs_to_post_config post_config.has_manys << post_has_many_comments_config - Vorpal::MasterConfig.new([post_config, comment_config]) + initialize_association_configs([post_config, comment_config]) association_config = comment_config.local_association_configs.first @@ -42,19 +42,27 @@ class Comment it 'builds an association_config for a has_many' do post_config.has_manys << post_has_many_comments_config - Vorpal::MasterConfig.new([post_config, comment_config]) + initialize_association_configs([post_config, comment_config]) expect(comment_config.local_association_configs.size).to eq(1) expect(post_config.local_association_configs.size).to eq(0) end end + def initialize_association_configs(configs) + main_config = Vorpal::MainConfig.new + configs.each do |config| + main_config.add_class_config(config) + end + main_config.initialize_association_configs + end + describe 'nice user feedback' do it 'lets the user know what the problem is when a configuration is missing' do - master_config = Vorpal::MasterConfig.new([]) + main_config = Vorpal::MainConfig.new expect { - master_config.config_for(String) + main_config.config_for(String) }.to raise_error(Vorpal::ConfigurationNotFound, "No configuration found for String") end end diff --git a/spec/unit/vorpal/db_loader_spec.rb b/spec/unit/vorpal/db_loader_spec.rb index dd5d6f8..397ae01 100644 --- a/spec/unit/vorpal/db_loader_spec.rb +++ b/spec/unit/vorpal/db_loader_spec.rb @@ -30,7 +30,7 @@ class Comment; end # attributes :length # end # - # master_config = Vorpal::MasterConfig.new([post_config, comment_config]) + # main_config = Vorpal::MainConfig.new([post_config, comment_config]) # # driver = Vorpal::Postgresql.new # @@ -39,29 +39,29 @@ class Comment; end # best_comment_db.update_attributes!(post_id: post_db.id) # # loader = Vorpal::DbLoader.new(false, driver) - # loaded_objects = loader.load_from_db([post_db.id], master_config.config_for(Post)) + # loaded_objects = loader.load_from_db([post_db.id], main_config.config_for(Post)) # p loaded_objects.all_objects # # expect(loaded_objects.all_objects.size).to eq(2) # - # repo = Vorpal::AggregateMapper.new(driver, master_config) + # repo = Vorpal::AggregateMapper.new(driver, main_config) # post = repo.load(post_db.id, Post) # p post # expect(post.comments.size).to eq(1) # end it 'loads an object once even when referred to by different associations of different types with stubs' do - post_config = Vorpal.build_class_config(Post, to: PostDB) do - attributes :name - belongs_to :best_comment, child_class: Comment - has_many :comments - end + engine = Vorpal.define do + map(Post, to: PostDB) do + attributes :name + belongs_to :best_comment, child_class: Comment + has_many :comments + end - comment_config = Vorpal.build_class_config(Comment, to: CommentDB) do - attributes :length + map(Comment, to: CommentDB) do + attributes :length + end end - Vorpal::MasterConfig.new([post_config, comment_config]) - best_comment_db = CommentDB.new best_comment_db.id = 99 post_db = PostDB.new(best_comment_id: best_comment_db.id) @@ -74,7 +74,7 @@ class Comment; end expect(driver).to receive(:load_by_foreign_key).and_return([best_comment_db]) loader = Vorpal::DbLoader.new(false, driver) - loaded_objects = loader.load_from_db([post_db.id], post_config) + loaded_objects = loader.load_from_db([post_db.id], engine.class_config(Post)) expect(loaded_objects.all_objects).to contain_exactly(post_db, best_comment_db) end diff --git a/spec/unit/vorpal/dsl/config_builder_spec.rb b/spec/unit/vorpal/dsl/config_builder_spec.rb index 9b851fe..074aa8a 100644 --- a/spec/unit/vorpal/dsl/config_builder_spec.rb +++ b/spec/unit/vorpal/dsl/config_builder_spec.rb @@ -1,18 +1,45 @@ require 'unit_spec_helper' require 'vorpal/dsl/config_builder' +require 'vorpal/driver/postgresql' describe Vorpal::Dsl::ConfigBuilder do class Tester; end - let(:builder) { Vorpal::Dsl::ConfigBuilder.new(Tester, {}, nil) } - describe 'mapping attributes' do it 'allows the \'attributes\' method to be called multiple times' do + builder = new_builder builder.attributes :first builder.attributes :second expect(builder.attributes_with_id).to eq([:id, :first, :second]) end end + + describe 'set primary key type' do + it 'has a default of :serial' do + builder = new_builder + + expect(builder.build.primary_key_type).to eq(:serial) + end + + it 'reads from the :primary_key_type option' do + builder = new_builder(primary_key_type: :uuid) + + expect(builder.build.primary_key_type).to eq(:uuid) + end + + it 'reads from the :id option' do + builder = new_builder(id: :uuid) + + expect(builder.build.primary_key_type).to eq(:uuid) + end + end + + private + + def new_builder(options = {}, db_driver = nil) + db_driver ||= instance_double(Vorpal::Driver::Postgresql, build_db_class: nil) + Vorpal::Dsl::ConfigBuilder.new(Tester, options, db_driver) + end end diff --git a/spec/unit/vorpal/loaded_objects_spec.rb b/spec/unit/vorpal/loaded_objects_spec.rb index 131a53a..baed468 100644 --- a/spec/unit/vorpal/loaded_objects_spec.rb +++ b/spec/unit/vorpal/loaded_objects_spec.rb @@ -14,7 +14,7 @@ def initialize(id:) it 'does not accept duplicate objects' do object = TestObject.new(id: 22) - config = Vorpal::ClassConfig.new({domain_class: Object}) + config = Vorpal::Config::ClassConfig.new(domain_class: Object, primary_key_type: :serial) subject.add(config, [object, object]) subject.add(config, [object])