Skip to content

Commit f03084a

Browse files
authored
Merge pull request rmosolgo#357 from rmosolgo/define-copy
feat(Define) support shallow redefinition with #redefine
2 parents 61f5ba9 + be10f63 commit f03084a

File tree

2 files changed

+73
-21
lines changed

2 files changed

+73
-21
lines changed

lib/graphql/define/instance_definable.rb

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -56,17 +56,23 @@ module Define
5656
# # Access it from metadata
5757
# subaru_baja.metadata[:all_wheel_drive] # => true
5858
#
59+
# @example Making a copy with an extended definition
60+
# # Create an instance with `.define`:
61+
# subaru_baja = Car.define do
62+
# make "Subaru"
63+
# model "Baja"
64+
# doors 4
65+
# end
66+
#
67+
# # Then extend it with `#redefine`
68+
# two_door_baja = subaru_baja.redefine do
69+
# doors 2
70+
# end
5971
module InstanceDefinable
6072
def self.included(base)
6173
base.extend(ClassMethods)
6274
end
6375

64-
# Set the definition block for this instance.
65-
# It can be run later with {#ensure_defined}
66-
def definition_proc=(defn_block)
67-
@definition_proc = defn_block
68-
end
69-
7076
# `metadata` can store arbitrary key-values with an object.
7177
#
7278
# @return [Hash<Object, Object>] Hash for user-defined storage
@@ -84,19 +90,21 @@ def metadata
8490
def define(**kwargs, &block)
8591
# make sure the previous definition_proc was executed:
8692
ensure_defined
87-
88-
@definition_proc = ->(obj) {
89-
kwargs.each do |keyword, value|
90-
public_send(keyword, value)
91-
end
92-
93-
if block
94-
instance_eval(&block)
95-
end
96-
}
93+
@pending_definition = Definition.new(kwargs, block)
9794
nil
9895
end
9996

97+
# Make a new instance of this class, then
98+
# re-run any definitions on that object.
99+
# @return [InstanceDefinable] A new instance, with any extended definitions
100+
def redefine(**kwargs, &block)
101+
ensure_defined
102+
new_instance = self.class.new
103+
applied_definitions.each { |defn| defn.apply(new_instance) }
104+
new_instance.define(**kwargs, &block)
105+
new_instance
106+
end
107+
100108
private
101109

102110
# Run the definition block if it hasn't been run yet.
@@ -105,15 +113,39 @@ def define(**kwargs, &block)
105113
# come from the definition block.
106114
# @return [void]
107115
def ensure_defined
108-
if @definition_proc
109-
defn_proc = @definition_proc
110-
@definition_proc = nil
111-
proxy = DefinedObjectProxy.new(self)
112-
proxy.instance_eval(&defn_proc)
116+
if @pending_definition
117+
defn = @pending_definition
118+
@pending_definition = nil
119+
defn.apply(self)
120+
applied_definitions << defn
113121
end
114122
nil
115123
end
116124

125+
def applied_definitions
126+
@applied_definitions ||= []
127+
end
128+
129+
130+
class Definition
131+
def initialize(define_keywords, define_proc)
132+
@define_keywords = define_keywords
133+
@define_proc = define_proc
134+
end
135+
136+
def apply(instance)
137+
defn_proxy = DefinedObjectProxy.new(instance)
138+
139+
@define_keywords.each do |keyword, value|
140+
defn_proxy.public_send(keyword, value)
141+
end
142+
143+
if @define_proc
144+
defn_proxy.instance_eval(&@define_proc)
145+
end
146+
end
147+
end
148+
117149
module ClassMethods
118150
# Create a new instance
119151
# and prepare a definition using its {.definitions}.

spec/graphql/define/instance_definable_spec.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,26 @@ class Vegetable
7373
end
7474
end
7575

76+
describe "#redefine" do
77+
it "re-runs definitions without modifying the original object" do
78+
arugula = Garden::Vegetable.define(name: "Arugula", color: :green)
79+
80+
red_arugula = arugula.redefine(color: :red)
81+
renamed_red_arugula = red_arugula.redefine do
82+
name "Renamed Red Arugula"
83+
end
84+
85+
assert_equal :green, arugula.metadata[:color]
86+
assert_equal "Arugula", arugula.name
87+
88+
assert_equal :red, red_arugula.metadata[:color]
89+
assert_equal "Arugula", red_arugula.name
90+
91+
assert_equal :red, renamed_red_arugula.metadata[:color]
92+
assert_equal "Renamed Red Arugula", renamed_red_arugula.name
93+
end
94+
end
95+
7696
describe "#metadata" do
7797
it "gets values from definitions" do
7898
arugula = Garden::Vegetable.define(name: "Arugula", color: :green)

0 commit comments

Comments
 (0)