From 9c9ee6ae204cdbaef96a59b0911ff61421f05bfa Mon Sep 17 00:00:00 2001 From: Steve Ross Date: Tue, 22 Apr 2014 12:47:50 -0700 Subject: [PATCH] Addresses issue Addresses issue #113. Allows specification of a proc, block, or sym for default values. --- README.md | 56 +++++++++++++++++++++++++++ motion/model/model.rb | 34 ++++++++++++++++- motion/version.rb | 2 +- spec/proc_defaults_spec.rb | 78 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 spec/proc_defaults_spec.rb diff --git a/README.md b/README.md index 2004a27..166754d 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,62 @@ class Task end ``` +A note on defaults, you can specify a proc, block or symbol for your default if you want to get fancy. The most obvious use case for this is that Ruby will optimize the assignment of an array so that a default of `[]` always points to the same object. Not exactly what is intended. Wrapping this in a proc causes a new array to be created. Here's an example: + +``` +class Foo + include MotionModel::Model + include MotionModel::ArrayModelAdapter + columns subject: { type: :array, default: ->{ [] } } +end +``` + +This is not constrained to initializing arrays. You can +initialize pretty much anything using a proc or block. +If you are specifying a block, make sure to use begin/end +instead of do/end because it makes Ruby happy. + +Here's a different example: + +``` +class Timely + include MotionModel::Model + include MotionModel::ArrayModelAdapter + columns ended_run_at: { type: :time, default: ->{ Time.now } } +end +``` +Note that this uses the "stubby proc" syntax. That is pretty much equivalent +to: + +``` +columns ended_run_at: { type: :time, default: lambda { Time.now } } +``` + +for the previous example. + +If you want to use a block, use the begin/end syntax: + +``` +columns ended_run_at: { type: :time, default: + begin + Time.now + end + } +``` +Finally, you can have the default call some class method as follows: + +``` +class Timely + include MotionModel::Model + include MotionModel::ArrayModelAdapter + columns unique_thingie: { type: :integer, default: :randomize } + + def self.randomize + rand 1_000_000 + end +end +``` + You can also include the `Validatable` module to get field validation. For example: ```ruby diff --git a/motion/model/model.rb b/motion/model/model.rb index e61ef4d..9d2ad17 100644 --- a/motion/model/model.rb +++ b/motion/model/model.rb @@ -785,8 +785,40 @@ def rebuild_relation(col, instance_or_collection, options = {}) # nodoc def unload_relation(col) end + def evaluate_default_value(column, value) + default = self.class.default(column) + + case default + when NilClass + {column => value} + when Proc + {column => default.call} + when Symbol + {column => self.send(column)} + else + {column => default} + end + end + + # issue #113. added ability to specify a proc or block + # for the default value. This allows for arrays to be + # created as unique. E.g.: + # + # class Foo + # include MotionModel::Model + # include MotionModel::ArrayModelAdapter + # columns subject: { type: :array, default: ->{ [] } } + # end + # + # ... + # + # This is not constrained to initializing arrays. You can + # initialize pretty much anything using a proc or block. + # If you are specifying a block, make sure to use begin/end + # instead of do/end because it makes Ruby happy. + def initialize_data_columns(column, value) #nodoc - self.attributes = {column => value || self.class.default(column)} + self.attributes = evaluate_default_value(column, value) end def column_as(col) #nodoc diff --git a/motion/version.rb b/motion/version.rb index 53e8935..4e3ef71 100644 --- a/motion/version.rb +++ b/motion/version.rb @@ -4,5 +4,5 @@ # or forward port their code to take advantage # of adapters. module MotionModel - VERSION = "0.5.2" + VERSION = "0.5.3" end diff --git a/spec/proc_defaults_spec.rb b/spec/proc_defaults_spec.rb new file mode 100644 index 0000000..218631e --- /dev/null +++ b/spec/proc_defaults_spec.rb @@ -0,0 +1,78 @@ +describe "proc for defaults" do + describe "accepts a proc or block for default" do + describe "accepts proc" do + class AcceptsProc + include MotionModel::Model + include MotionModel::ArrayModelAdapter + columns subject: { type: :array, default: ->{ [] } } + end + + before do + @test1 = AcceptsProc.create + @test2 = AcceptsProc.create + end + + it "initializes array type using proc call" do + @test1.subject.should.be == @test2.subject + end + end + + describe "accepts block" do + class AcceptsBlock + include MotionModel::Model + include MotionModel::ArrayModelAdapter + columns subject: { + type: :array, default: begin + [] + end + } + end + + before do + @test1 = AcceptsBlock.create + @test2 = AcceptsBlock.create + end + + it "initializes array type using begin/end block call" do + @test1.subject.should.be == @test2.subject + end + end + + describe "accepts symbol" do + class AcceptsSym + include MotionModel::Model + include MotionModel::ArrayModelAdapter + columns subject: { type: :integer, default: :randomize } + + def self.randomize + rand 1_000_000 + end + end + + before do + @test1 = AcceptsSym.create + @test2 = AcceptsSym.create + end + + it "initializes column by calling a method" do + @test1.subject.should.be == @test2.subject + end + end + + describe "scalar defaults still work" do + class AcceptsScalars + include MotionModel::Model + include MotionModel::ArrayModelAdapter + columns subject: { type: :integer, default: 42 } + end + + before do + @test1 = AcceptsScalars.create + end + + it "initializes column as normal" do + @test1.subject.should == 42 + end + end + end +end