diff --git a/lib/safe_values/version.rb b/lib/safe_values/version.rb index eab65bd..c2c371b 100644 --- a/lib/safe_values/version.rb +++ b/lib/safe_values/version.rb @@ -1,3 +1,3 @@ module SafeValues - VERSION = "1.0.2" + VERSION = "1.1.0" end diff --git a/lib/value.rb b/lib/value.rb index 6af177c..a91e6a6 100644 --- a/lib/value.rb +++ b/lib/value.rb @@ -13,7 +13,9 @@ # `ValueType = Value.new(:a, :b, c: default_value)`. The default values to # optional arguments are saved at class creation time and supplied as default # constructor arguments to instances. Default values are aliased, so providing -# mutable defaults is discouraged. +# mutable defaults is discouraged. Lazily-evaluated defaults can be provided by +# supplying `Value.lazy { ... }` as a default. The lazy block will be evaluated +# at each instantiation time. # # Two instance constructors are provided, with positional and keyword arguments. # @@ -25,6 +27,8 @@ # Value types may be constructed with keyword arguments using `with`. # For example: `ValueType.with(a: 1, b: 2, c: 3)` class Value < Struct + LazyDefault = Struct.new(:proc) + class << self def new(*required_args, **optional_args, &block) arguments = {} @@ -39,8 +43,14 @@ def new(*required_args, **optional_args, &block) keyword_constructor = generate_keyword_constructor(arguments) class_method_module = Module.new do module_eval(keyword_constructor) - define_method(:__constructor_default) do |name| - optional_args.fetch(name) + + optional_args.each do |name, value| + method_name = :"__constructor_default_#{name}" + if value.is_a?(LazyDefault) + define_method(method_name, &value.proc) + else + define_method(method_name) { value } + end end end clazz.extend(class_method_module) @@ -57,6 +67,10 @@ def new(*required_args, **optional_args, &block) clazz end + def lazy(&proc) + LazyDefault.new(proc) + end + private def validate_names(*params) @@ -73,7 +87,7 @@ def validate_names(*params) # # For a Value.new(:a, b: x), will define the method: # - # def initialize(a, b = self.class.__constructor_default(:b)) + # def initialize(a, b = self.class.__constructor_default_b) # super(a, b) # freeze # end @@ -82,7 +96,7 @@ def generate_constructor(arguments) if required arg_name else - "#{arg_name} = self.class.__constructor_default(:#{arg_name})" + "#{arg_name} = self.class.__constructor_default_#{arg_name}" end end @@ -100,7 +114,7 @@ def initialize(#{params.join(", ")}) # # For a Value.new(:a, b: x), will define the (class) method: # - # def with(a:, b: __constructor_default(:b)) + # def with(a:, b: __constructor_default_b) # self.new(a, b) # end def generate_keyword_constructor(arguments) @@ -108,7 +122,7 @@ def generate_keyword_constructor(arguments) if required "#{arg_name}:" else - "#{arg_name}: __constructor_default(:#{arg_name})" + "#{arg_name}: __constructor_default_#{arg_name}" end end diff --git a/spec/unit/value_spec.rb b/spec/unit/value_spec.rb index 7c8995d..64ff8c0 100644 --- a/spec/unit/value_spec.rb +++ b/spec/unit/value_spec.rb @@ -90,6 +90,33 @@ end end + context "with lazy defaults" do + let(:next_value) do + x = 0 + ->() { x += 1 } + end + + let(:value) { Value.new(a: Value.lazy(&next_value)) } + + it "can construct an instance with positional lazy arguments" do + v = value.new + expect(v.a).to eq(1) + v = value.new + expect(v.a).to eq(2) + v = value.new(-1) + expect(v.a).to eq(-1) + end + + it "can construct an instance with keyword lazy arguments" do + v = value.with() + expect(v.a).to eq(1) + v = value.with() + expect(v.a).to eq(2) + v = value.with(a: -1) + expect(v.a).to eq(-1) + end + end + context "with a class body" do let(:value) do Value.new(:a) do