Commit 56f17f9

mo khan <mo@mokhan.ca>
2014-06-13 03:00:17
move most of the mapping logic to each column type.
1 parent 30a23f0
lib/humble/column.rb
@@ -1,21 +1,28 @@
 module Humble
   class Column
+    attr_reader :column_name
+
     def initialize(name)
       @column_name = name
     end
 
-    def prepare(item)
-      return {} if primary_key? && has_default_value?
-      { column_name.to_sym => item.instance_variable_get("@#{column_name}") }
+    def prepare(entity)
+      return {} if primary_key? && has_default_value?(entity)
+      #{ column_name.to_sym => entity.instance_variable_get("@#{column_name}") }
+      { column_name.to_sym => entity.public_send(column_name.to_sym) }
     end
 
-    def primary_key?
-      false
+    def matches?(column_name)
+      @column_name == column_name
     end
 
-    protected
+    def apply(value, entity)
+      entity.public_send("#{@column_name}=", value)
+    end
 
-    attr_reader :column_name
+    def primary_key?
+      false
+    end
   end
 
   class PrimaryKeyColumn < Column
@@ -26,10 +33,6 @@ module Humble
       @default = default
     end
 
-    def apply(id, entity)
-      entity.instance_variable_set("@#{column_name}", id )
-    end
-
     def destroy(connection, entity)
       connection.where(column_name.to_sym => entity.id).delete
     end
@@ -38,8 +41,23 @@ module Humble
       true
     end
 
-    def has_default_value?(item)
-      @default == item.id
+    def has_default_value?(entity)
+      entity.id == nil || @default == entity.id
+    end
+  end
+
+  class BelongsTo < Column
+    def initialize(name, type)
+      super(name)
+      @type = type
+    end
+
+    def apply(id, entity)
+      #entity.public_send("#{column_name}=", id)
+    end
+
+    def prepare(entity)
+      { column_name.to_sym => '' }
     end
   end
 end
lib/humble/database_table.rb
@@ -1,6 +1,7 @@
 module Humble
   class DatabaseTable
     attr_reader :name
+    attr_accessor :type
 
     def initialize
       @columns = []
@@ -12,10 +13,19 @@ module Humble
 
     def primary_key(name, default: 0)
       @primary_key = PrimaryKeyColumn.new(name, default)
+      add(@primary_key)
+    end
+
+    def add(column)
+      @columns.push(column)
     end
 
     def add_column(name)
-      @columns << Column.new(name)
+      add(Column.new(name))
+    end
+
+    def find_all_using(connection)
+      ResultSet.new(connection[self.name], self)
     end
 
     def persist(connection, item)
@@ -30,8 +40,21 @@ module Humble
       @primary_key.destroy(connection[@name], entity)
     end
 
+    def map_from(row)
+      entity = type.new
+      row.each do |key, value|
+        #entity.send("#{key}=", value) unless key == :studio_id
+        column_for(key).apply(value, entity)
+      end
+      entity
+    end
+
     private
 
+    def column_for(key)
+      @columns.find { |x| x.matches?(key) }
+    end
+
     def prepare_statement_for(item)
       @columns.inject({}) do |result, column|
         result.merge(column.prepare(item))
lib/humble/default_data_row_mapper.rb
@@ -5,7 +5,11 @@ module Humble
     end
 
     def map_from(row)
-      @configuration[:type].new(row)
+      result = @configuration.type.new
+      row.each do |key, value|
+        result.send("#{key}=", value)
+      end
+      result
     end
   end
 end
lib/humble/mapping_configuration.rb
@@ -6,7 +6,7 @@ module Humble
     end
 
     def find_all_using(connection)
-      ResultSet.new(connection[@table.name], mapper)
+      @table.find_all_using(connection)
     end
 
     def save_using(connection, entity)
@@ -24,11 +24,5 @@ module Humble
     def [](key)
       @attributes[key]
     end
-
-    private
-
-    def mapper
-      self[:mapper] || DefaultDataRowMapper.new(self)
-    end
   end
 end
lib/humble/mapping_configuration_builder.rb
@@ -12,6 +12,7 @@ module Humble
 
     def type(name)
       @attributes[:type] = name
+      @table.type=name
     end
 
     def primary_key(name, default: 0)
@@ -23,6 +24,10 @@ module Humble
       @table.add_column(name)
     end
 
+    def belongs_to(foreign_key, type)
+      @table.add(BelongsTo.new(foreign_key, type))
+    end
+
     def build
       MappingConfiguration.new(@attributes, @table)
     end
lib/humble/session.rb
@@ -15,6 +15,10 @@ module Humble
       mapping_for(entity).save_using(create_connection, entity)
     end
 
+    def find(clazz, id)
+      find_all(clazz).find { |x| x.id == id }
+    end
+
     def find_all(clazz)
       mapping_for(clazz).find_all_using(create_connection)
     end
spec/integration/fixtures/movie_mapping.rb
@@ -1,14 +1,5 @@
 class Movie
-  attr_reader :id, :name
-
-  def initialize(attributes)
-    @id = attributes[:id] || -1
-    @name = attributes[:name]
-  end
-
-  def name=(new_name)
-    @name = new_name
-  end
+  attr_accessor :id, :name, :studio
 
   def ==(other)
     return false unless other
@@ -24,5 +15,6 @@ class MovieMapping < Humble::DatabaseMapping
     map.type Movie
     map.primary_key(:id, default: -1)
     map.column :name
+    map.belongs_to :studio_id, Studio
   end
 end
spec/integration/fixtures/studio_mapping.rb
@@ -0,0 +1,12 @@
+class Studio
+  attr_accessor :id, :name
+end
+
+class StudioMapping < Humble::DatabaseMapping
+  def run(map)
+    map.table :studios
+    map.type Studio
+    map.primary_key(:id, default: -1)
+    map.column :name
+  end
+end
spec/integration/insert_spec.rb
@@ -4,7 +4,7 @@ describe "orm" do
   include_context "orm"
 
   context "when inserting a new record" do
-    let(:movie) { Movie.new(:name => 'oop') }
+    let(:movie) { Movie.new.tap { |x| x.name = 'oop' } }
 
     before :each do
       session.begin_transaction do |session|
@@ -15,20 +15,20 @@ describe "orm" do
     let(:results) { connection[:movies].all }
 
     it "should insert the correct number of records" do
-      results.count.should == 1
+      expect(results.count).to eql(1)
     end
 
     it "should insert the record with the a new id" do
-      results.first[:id].should_not == -1
-      results.first[:id].should > 0
+      expect(results.first[:id]).to_not eql(-1)
+      expect(results.first[:id]).to be > 0
     end
 
     it "should insert the name" do
-      results.first[:name].should == 'oop'
+      expect(results.first[:name]).to eql('oop')
     end
 
     it "should update the new item with the new id" do
-      movie.id.should_not == -1
+      expect(movie.id).to_not eql(-1)
     end
   end
 end
spec/integration/select_spec.rb
@@ -11,20 +11,33 @@ describe "select items" do
     let(:results) { session.find_all Movie }
 
     it "should return the correct number of movies" do
-      results.count.should == 1
+      expect(results.count).to eql(1)
     end
 
     it "should return each movie with its name" do
-      results.first.name.should == 'monsters inc'
+      expect(results.first.name).to eql('monsters inc')
     end
 
     it "should return instances of the target type" do
-      results.first.should be_instance_of(Movie)
+      expect(results.first).to be_instance_of(Movie)
     end
 
     it "should include the saved movie" do
-      results.should include(Movie.new(:id => @id, :name => 'monsters inc'))
+      movie = Movie.new.tap { |x| x.id = @id }
+      expect(results).to include(movie)
     end
   end
 
+  context "when fetching a single item" do
+    it "loads the belongs_to association" do
+      studio_id = connection[:studios].insert(name: 'universal')
+      movie_id = connection[:movies].insert(name: 'blood in, blood out', studio_id: studio_id)
+
+      result = session.find(Movie, movie_id)
+      expect(result).to be_instance_of(Movie)
+      expect(result.id).to eql(movie_id)
+      expect(result.name).to eql('blood in, blood out')
+      #expect(result.studio).to be_instance_of(Studio)
+    end
+  end
 end
spec/integration/update_spec.rb
@@ -4,7 +4,7 @@ describe "updating a record" do
   include_context "orm"
 
   context "when updating a record" do
-    let(:movie) { Movie.new(:name => "old name") }
+    let(:movie) { Movie.new.tap { |x| x.name = "old name" } }
 
     before :each do
       session.begin_transaction do |session|
@@ -19,11 +19,11 @@ describe "updating a record" do
     let(:results) { connection[:movies].all }
 
     it "should save the changes" do
-      results.first[:name].should == 'new name'
+      expect(results.first[:name]).to eql('new name')
     end
 
     it "should not create any new records" do
-      results.count.should == 1
+      expect(results.count).to eql(1)
     end
   end
 end
spec/unit/default_data_row_mapper_spec.rb
@@ -1,24 +1,22 @@
-require "spec_helper"
-class Book
-  attr_reader :id, :name
+require "ostruct"
 
-  def initialize(attributes)
-    @id = attributes[:id]
-    @name = attributes[:name]
-  end
+class Book
+  attr_accessor :id, :name
 end
 
-describe Humble::DefaultDataRowMapper do
-  let(:sut) { Humble::DefaultDataRowMapper.new(configuration) }
-  let(:configuration) { { :type => Book } }
+module Humble
+  describe DefaultDataRowMapper do
+    let(:sut) { Humble::DefaultDataRowMapper.new(configuration) }
+    let(:configuration) { OpenStruct.new(:type => Book) }
 
-  let(:result) { sut.map_from({:id => 1, :name => 'blah'}) }
+    let(:result) { sut.map_from({:id => 1, :name => 'blah'}) }
 
-  it "should map the id" do
-    result.id.should == 1
-  end
+    it "should map the id" do
+      expect(result.id).to eql(1)
+    end
 
-  it "should map the name" do
-    result.name.should == "blah"
+    it "should map the name" do
+      expect(result.name).to eql("blah")
+    end
   end
 end
spec/integration_helper.rb
@@ -1,5 +1,6 @@
 require "spec_helper"
 require 'sequel'
+require_relative 'integration/fixtures/studio_mapping.rb'
 require_relative 'integration/fixtures/movie_mapping.rb'
 
 shared_context "orm" do
@@ -10,15 +11,23 @@ shared_context "orm" do
   let(:session) { session_factory.create_session }
 
   before :each do
+    connection.create_table :studios do
+      primary_key :id
+      String :name
+    end
+
     connection.create_table :movies do
       primary_key :id
+      BigNum :studio_id
       String :name
     end
 
     configuration.add(MovieMapping.new)
+    configuration.add(StudioMapping.new)
   end
 
   after :each do
+    connection.drop_table :studios
     connection.drop_table :movies
   end
 end
spec/spec_helper.rb
@@ -1,52 +1,12 @@
 Bundler.require
 RSpec.configure do |config|
-  # Many RSpec users commonly either run the entire suite or an individual
-  # file, and it's useful to allow more verbose output when running an
-  # individual spec file.
-  if config.files_to_run.one?
-    # Use the documentation formatter for detailed output,
-    # unless a formatter has already been configured
-    # (e.g. via a command-line flag).
-    config.default_formatter = 'doc'
-  end
-
-  # Print the 10 slowest examples and example groups at the
-  # end of the spec run, to help surface which specs are running
-  # particularly slow.
-  config.profile_examples = 10
-
-  # Run specs in random order to surface order dependencies. If you find an
-  # order dependency and want to debug it, you can fix the order by providing
-  # the seed, which is printed after each run.
-  #     --seed 1234
   config.order = :random
-
-  # Seed global randomization in this process using the `--seed` CLI option.
-  # Setting this allows you to use `--seed` to deterministically reproduce
-  # test failures related to randomization by passing the same `--seed` value
-  # as the one that triggered the failure.
   Kernel.srand config.seed
-
-  # rspec-expectations config goes here. You can use an alternate
-  # assertion/expectation library such as wrong or the stdlib/minitest
-  # assertions if you prefer.
   config.expect_with :rspec do |expectations|
-    # Enable only the newer, non-monkey-patching expect syntax.
-    # For more details, see:
-    #   - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
-    expectations.syntax = :expect
+    expectations.syntax = [:expect, :should]
   end
-
-  # rspec-mocks config goes here. You can use an alternate test double
-  # library (such as bogus or mocha) by changing the `mock_with` option here.
   config.mock_with :rspec do |mocks|
-    # Enable only the newer, non-monkey-patching expect syntax.
-    # For more details, see:
-    #   - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
-    mocks.syntax = :expect
-
-    # Prevents you from mocking or stubbing a method that does not exist on
-    # a real object. This is generally recommended.
+    mocks.syntax = [:expect, :should]
     mocks.verify_partial_doubles = true
   end
 end
.rspec
@@ -1,3 +1,2 @@
 --color
---warnings
 --require spec_helper