fixtury 0.4.1 → 1.0.0.beta2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.ruby-version +1 -1
- data/Gemfile.lock +48 -44
- data/README.md +4 -4
- data/fixtury.gemspec +6 -6
- data/lib/fixtury/configuration.rb +117 -0
- data/lib/fixtury/definition.rb +34 -38
- data/lib/fixtury/definition_executor.rb +23 -52
- data/lib/fixtury/dependency.rb +52 -0
- data/lib/fixtury/dependency_store.rb +45 -0
- data/lib/fixtury/errors.rb +81 -0
- data/lib/fixtury/hooks.rb +90 -0
- data/lib/fixtury/locator.rb +52 -23
- data/lib/fixtury/locator_backend/common.rb +17 -19
- data/lib/fixtury/locator_backend/global_id.rb +32 -0
- data/lib/fixtury/locator_backend/memory.rb +9 -8
- data/lib/fixtury/mutation_observer.rb +140 -0
- data/lib/fixtury/path_resolver.rb +45 -0
- data/lib/fixtury/railtie.rb +4 -0
- data/lib/fixtury/reference.rb +12 -10
- data/lib/fixtury/schema.rb +47 -225
- data/lib/fixtury/schema_node.rb +175 -0
- data/lib/fixtury/store.rb +168 -84
- data/lib/fixtury/test_hooks.rb +125 -101
- data/lib/fixtury/version.rb +1 -1
- data/lib/fixtury.rb +84 -12
- metadata +35 -35
- data/lib/fixtury/errors/already_defined_error.rb +0 -13
- data/lib/fixtury/errors/circular_dependency_error.rb +0 -13
- data/lib/fixtury/errors/fixture_not_defined_error.rb +0 -13
- data/lib/fixtury/errors/option_collision_error.rb +0 -13
- data/lib/fixtury/errors/schema_frozen_error.rb +0 -13
- data/lib/fixtury/errors/unrecognizable_locator_error.rb +0 -11
- data/lib/fixtury/locator_backend/globalid.rb +0 -32
- data/lib/fixtury/path.rb +0 -36
- data/lib/fixtury/tasks.rake +0 -10
data/lib/fixtury/schema.rb
CHANGED
@@ -1,251 +1,73 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "fixtury/definition"
|
4
|
-
require "fixtury/path"
|
5
|
-
require "fixtury/errors/already_defined_error"
|
6
|
-
require "fixtury/errors/fixture_not_defined_error"
|
7
|
-
require "fixtury/errors/schema_frozen_error"
|
8
|
-
require "fixtury/errors/option_collision_error"
|
9
|
-
|
10
3
|
module Fixtury
|
4
|
+
# A Fixtury::SchemaNode implementation that represents a top-level schema
|
5
|
+
# or a namespace within a schema.
|
11
6
|
class Schema
|
12
7
|
|
13
|
-
|
14
|
-
|
15
|
-
def initialize(parent:, name:)
|
16
|
-
@name = name
|
17
|
-
@parent = parent
|
18
|
-
@relative_name = @name.split("/").last
|
19
|
-
@around_fixture_definition = nil
|
20
|
-
@options = {}
|
21
|
-
@frozen = false
|
22
|
-
reset!
|
23
|
-
end
|
24
|
-
|
25
|
-
def merge_options(opts = {})
|
26
|
-
opts.each_pair do |k, v|
|
27
|
-
if options.key?(k) && options[k] != v
|
28
|
-
raise ::Fixtury::Errors::OptionCollisionError.new(name, k, options[k], v)
|
29
|
-
end
|
30
|
-
|
31
|
-
options[k] = v
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
def around_fixture(&block)
|
36
|
-
@around_fixture_definition = block
|
37
|
-
end
|
38
|
-
|
39
|
-
def around_fixture_hook(executor, &definition)
|
40
|
-
maybe_invoke_parent_around_fixture_hook(executor) do
|
41
|
-
if around_fixture_definition.nil?
|
42
|
-
yield
|
43
|
-
else
|
44
|
-
around_fixture_definition.call(executor, definition)
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
def maybe_invoke_parent_around_fixture_hook(executor, &block)
|
50
|
-
return yield unless parent
|
51
|
-
|
52
|
-
parent.around_fixture_hook(executor, &block)
|
53
|
-
end
|
8
|
+
include ::Fixtury::SchemaNode
|
54
9
|
|
55
|
-
|
56
|
-
|
57
|
-
|
10
|
+
# @param name [String] the name of the schema, defaults to "" to represent
|
11
|
+
# a new top-level schema.
|
12
|
+
def initialize(name: "", **options)
|
13
|
+
super(name: name, **options)
|
58
14
|
end
|
59
15
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
def frozen?
|
65
|
-
!!@frozen
|
66
|
-
end
|
67
|
-
|
68
|
-
def top_level_schema
|
69
|
-
top_level_schema? ? self : parent.top_level_schema
|
70
|
-
end
|
71
|
-
|
72
|
-
def top_level_schema?
|
73
|
-
parent.nil?
|
16
|
+
# Object#acts_like? adherence.
|
17
|
+
def acts_like_fixtury_schema?
|
18
|
+
true
|
74
19
|
end
|
75
20
|
|
21
|
+
# Open up self for child definitions.
|
22
|
+
# @param block [Proc] The block to be executed in the context of the schema.
|
23
|
+
# @return [Fixtury::Schema] self
|
76
24
|
def define(&block)
|
77
|
-
ensure_not_frozen!
|
78
25
|
instance_eval(&block)
|
79
26
|
self
|
80
27
|
end
|
81
28
|
|
82
|
-
#
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
definitions.keys.sort.each do |key|
|
87
|
-
out << "#{indent} defn:#{key}"
|
88
|
-
end
|
89
|
-
|
90
|
-
children.keys.sort.each do |key|
|
91
|
-
child = children[key]
|
92
|
-
out << child.structure("#{indent} ")
|
93
|
-
end
|
94
|
-
|
95
|
-
out.join("\n")
|
96
|
-
end
|
97
|
-
|
98
|
-
def namespace(name, options = {}, &block)
|
99
|
-
ensure_not_frozen!
|
100
|
-
ensure_no_conflict!(name: name, definitions: true, namespaces: false)
|
101
|
-
|
102
|
-
child = find_or_create_child_schema(name: name, options: options)
|
103
|
-
child.instance_eval(&block) if block_given?
|
104
|
-
child
|
105
|
-
end
|
106
|
-
|
107
|
-
def fixture(name, options = {}, &block)
|
108
|
-
ensure_not_frozen!
|
109
|
-
ensure_no_conflict!(name: name, definitions: true, namespaces: true)
|
110
|
-
create_child_definition(name: name, options: options, &block)
|
111
|
-
end
|
112
|
-
|
113
|
-
def enhance(name, &block)
|
114
|
-
ensure_not_frozen!
|
115
|
-
definition = get_definition!(name)
|
116
|
-
definition.enhance(&block)
|
117
|
-
definition
|
118
|
-
end
|
119
|
-
|
120
|
-
def merge(other_ns)
|
121
|
-
ensure_not_frozen!
|
122
|
-
other_ns.definitions.each_pair do |name, dfn|
|
123
|
-
fixture(name, dfn.options, &dfn.callable)
|
124
|
-
dfn.enhancements.each do |e|
|
125
|
-
enhance(name, &e)
|
126
|
-
end
|
127
|
-
end
|
128
|
-
|
129
|
-
other_ns.children.each_pair do |name, other_ns_child|
|
130
|
-
namespace(name, other_ns_child.options) do
|
131
|
-
merge(other_ns_child)
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
around_fixture(&other_ns.around_fixture_definition) if other_ns.around_fixture_definition
|
136
|
-
|
137
|
-
self
|
138
|
-
end
|
139
|
-
|
140
|
-
def get_definition!(name)
|
141
|
-
dfn = get_definition(name)
|
142
|
-
raise ::Fixtury::Errors::FixtureNotDefinedError, name unless dfn
|
143
|
-
|
144
|
-
dfn
|
145
|
-
end
|
146
|
-
|
147
|
-
def get_definition(name)
|
148
|
-
path = ::Fixtury::Path.new(namespace: self.name, path: name)
|
149
|
-
top_level = top_level_schema
|
150
|
-
|
151
|
-
dfn = nil
|
152
|
-
path.possible_absolute_paths.each do |abs_path|
|
153
|
-
*namespaces, definition_name = abs_path.split("/")
|
154
|
-
|
155
|
-
namespaces.shift if namespaces.first == top_level.name
|
156
|
-
target = top_level
|
157
|
-
|
158
|
-
namespaces.each do |ns|
|
159
|
-
next if ns.empty?
|
160
|
-
|
161
|
-
target = target.children[ns]
|
162
|
-
break unless target
|
163
|
-
end
|
164
|
-
|
165
|
-
dfn = target.definitions[definition_name] if target
|
166
|
-
return dfn if dfn
|
167
|
-
end
|
168
|
-
|
169
|
-
nil
|
29
|
+
# Returns "schema" if top-level, otherwise returns "namespace".
|
30
|
+
# @return [String] the type of the schema node.
|
31
|
+
def schema_node_type
|
32
|
+
first_ancestor? ? "schema" : "namespace"
|
170
33
|
end
|
171
34
|
|
172
|
-
|
173
|
-
|
174
|
-
|
35
|
+
# Create a child schema at the given relative name. If a child by the name
|
36
|
+
# already exists it will be reopened as long as it's a fixtury schema.
|
37
|
+
#
|
38
|
+
# @param relative_name [String] The relative name of the child schema.
|
39
|
+
# @param options [Hash] Additional options for the child schema, applied after instantiation.
|
40
|
+
# @param block [Proc] A block of code to be executed in the context of the child schema.
|
41
|
+
# @return [Fixtury::Schema] The child schema.
|
42
|
+
# @raise [Fixtury::Errors::AlreadyDefinedError] if the child is already defined and not a fixtury schema.
|
43
|
+
def namespace(relative_name, **options, &block)
|
44
|
+
child = get("./#{relative_name}")
|
175
45
|
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
namespaces.shift if namespaces.first == top_level.name
|
180
|
-
target = top_level
|
181
|
-
|
182
|
-
namespaces.each do |ns|
|
183
|
-
next if ns.empty?
|
184
|
-
|
185
|
-
target = target.children[ns]
|
186
|
-
break unless target
|
187
|
-
end
|
188
|
-
|
189
|
-
return target if target
|
46
|
+
if child && !child.acts_like?(:fixtury_schema)
|
47
|
+
raise Errors::AlreadyDefinedError, child.pathname
|
190
48
|
end
|
191
49
|
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
protected
|
196
|
-
|
197
|
-
def find_child_schema(name:)
|
198
|
-
children[name.to_s]
|
199
|
-
end
|
200
|
-
|
201
|
-
def find_or_create_child_schema(name:, options:)
|
202
|
-
name = name.to_s
|
203
|
-
child = find_child_schema(name: name)
|
204
|
-
child ||= begin
|
205
|
-
children[name] = begin
|
206
|
-
child_name = build_child_name(name: name)
|
207
|
-
self.class.new(name: child_name, parent: self)
|
208
|
-
end
|
209
|
-
end
|
210
|
-
child.merge_options(options)
|
50
|
+
child ||= self.class.new(name: relative_name, parent: self)
|
51
|
+
child.apply_options!(options)
|
52
|
+
child.instance_eval(&block) if block_given?
|
211
53
|
child
|
212
54
|
end
|
213
55
|
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
arr = ["", self.name, name]
|
230
|
-
arr.join("/").gsub(%r{/{2,}}, "/")
|
231
|
-
end
|
232
|
-
|
233
|
-
def ensure_no_conflict!(name:, namespaces:, definitions:)
|
234
|
-
if definitions
|
235
|
-
definition = find_child_definition(name: name)
|
236
|
-
raise ::Fixtury::Errors::AlreadyDefinedError, definition.name if definition
|
237
|
-
end
|
238
|
-
|
239
|
-
if namespaces
|
240
|
-
ns = find_child_schema(name: name)
|
241
|
-
raise ::Fixtury::Errors::AlreadyDefinedError, ns.name if ns
|
242
|
-
end
|
243
|
-
end
|
244
|
-
|
245
|
-
def ensure_not_frozen!
|
246
|
-
return unless frozen?
|
247
|
-
|
248
|
-
raise ::Fixtury::Errors::SchemaFrozenError
|
56
|
+
# Create a fixture definition at the given relative name. If the name is already
|
57
|
+
# used, a Fixtury::Errors::AlreadyDefinedError will be raised.
|
58
|
+
#
|
59
|
+
# @param relative_name [String] The relative name of the fixture.
|
60
|
+
# @param options [Hash] Additional options for the fixture.
|
61
|
+
# @param block [Proc] The block representing the build function of the fixture.
|
62
|
+
# @return [Fixtury::Definition] The fixture definition.
|
63
|
+
#
|
64
|
+
def fixture(relative_name, **options, &block)
|
65
|
+
::Fixtury::Definition.new(
|
66
|
+
name: relative_name,
|
67
|
+
parent: self,
|
68
|
+
**options,
|
69
|
+
&block
|
70
|
+
)
|
249
71
|
end
|
250
72
|
|
251
73
|
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
module Fixtury
|
2
|
+
# This module is used to provide a common interface for all nodes in the schema tree.
|
3
|
+
# Namespaces and fixture definitions adhere to this interface and are provided with
|
4
|
+
# common behaviors for registration, traversal, inspection, etc
|
5
|
+
module SchemaNode
|
6
|
+
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
VALID_NODE_NAME = /^[a-zA-Z0-9_]*$/
|
10
|
+
|
11
|
+
included do
|
12
|
+
attr_reader :name, :pathname, :parent, :first_ancestor, :children, :options
|
13
|
+
end
|
14
|
+
|
15
|
+
# Constructs a new SchemaNode object.
|
16
|
+
#
|
17
|
+
# @param name [String] The relative name of the node.
|
18
|
+
# @param parent [Object] The parent node of the node.
|
19
|
+
# @param options [Hash] Additional options for the node.
|
20
|
+
# @return [Fixtury::SchemaNode] The new SchemaNode object.
|
21
|
+
# @raise [ArgumentError] if the name does not match the VALID_NODE_NAME regex.
|
22
|
+
def initialize(name:, parent: nil, **options)
|
23
|
+
name = name.to_s
|
24
|
+
raise ArgumentError, "#{name.inspect} is an invalid node name" unless name.match?(VALID_NODE_NAME)
|
25
|
+
|
26
|
+
@name = name
|
27
|
+
@parent = parent
|
28
|
+
@pathname = File.join(*[parent&.pathname, "/", @name].compact).to_s
|
29
|
+
@children = {}
|
30
|
+
@options = {}
|
31
|
+
apply_options!(options)
|
32
|
+
@first_ancestor = @parent&.first_ancestor || self
|
33
|
+
@parent&.add_child(self)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Inspect the SchemaNode object without representing the parent or children to avoid
|
37
|
+
# large prints.
|
38
|
+
#
|
39
|
+
# @return [String] The inspection string.
|
40
|
+
def inspect
|
41
|
+
"#{self.class}(pathname: #{pathname.inspect}, children: #{children.size})"
|
42
|
+
end
|
43
|
+
|
44
|
+
# An identifier used during the printing of the tree structure.
|
45
|
+
#
|
46
|
+
# @return [String] The demodularized class name.
|
47
|
+
def schema_node_type
|
48
|
+
self.class.name.demodulize.underscore
|
49
|
+
end
|
50
|
+
|
51
|
+
# Adherance to the acts_like? interface
|
52
|
+
def acts_like_fixtury_schema_node?
|
53
|
+
true
|
54
|
+
end
|
55
|
+
|
56
|
+
# Adds child to the node's children hash as long as another is not already defined.
|
57
|
+
#
|
58
|
+
# @param child [Fixtury::SchemaNode] The child node to add.
|
59
|
+
# @raise [Fixtury::Errors::AlreadyDefinedError] if the child is already defined and not the provided child.
|
60
|
+
# @return [Fixtury::Errors::AlreadyDefinedError] the child that was suscessfully added
|
61
|
+
def add_child(child)
|
62
|
+
if children.key?(child.name) && children[child.name] != child
|
63
|
+
raise Errors::AlreadyDefinedError, child.pathname
|
64
|
+
end
|
65
|
+
|
66
|
+
children[child.name] = child
|
67
|
+
end
|
68
|
+
|
69
|
+
# Is the current node the first ancestor?
|
70
|
+
#
|
71
|
+
# @return [TrueClass, FalseClass] `true` if the node is the first ancestor, `false` otherwise.
|
72
|
+
def first_ancestor?
|
73
|
+
parent.nil?
|
74
|
+
end
|
75
|
+
|
76
|
+
# Determines the isolation key in a top-down manner. It first accepts an isolation key
|
77
|
+
# set by the parent, then it checks for an isolation key set by the node itself. If no
|
78
|
+
# isolation key is found, it defaults to the node's name unless default is set to falsy.
|
79
|
+
#
|
80
|
+
# @param default [TrueClass, FalseClass, String] if no isolation key is present, what should the default value be?
|
81
|
+
# @option default [true] The default value is the node's name.
|
82
|
+
# @option default [String] The default value is a custom string.
|
83
|
+
# @option default [false, nil, ""] No isolation key should be represented
|
84
|
+
# @return [String, NilClass] The isolation key.
|
85
|
+
def isolation_key(default: true)
|
86
|
+
from_parent = parent&.isolation_key(default: nil)
|
87
|
+
return from_parent if from_parent
|
88
|
+
|
89
|
+
value = options[:isolate] || default
|
90
|
+
value = (value == true ? pathname : value&.to_s).presence
|
91
|
+
value = (value == "/" ? nil : value) # special case to accommodate root nodes
|
92
|
+
value.presence
|
93
|
+
end
|
94
|
+
|
95
|
+
# Performs get() but raises if the result is nil.
|
96
|
+
# @raise [Fixtury::Errors::SchemaNodeNotDefinedError] if the search does not return a node.
|
97
|
+
# (see #get)
|
98
|
+
def get!(search)
|
99
|
+
thing = get(search)
|
100
|
+
raise Errors::SchemaNodeNotDefinedError.new(pathname, search) unless thing
|
101
|
+
|
102
|
+
thing
|
103
|
+
end
|
104
|
+
|
105
|
+
# Retrieves a node in the tree relative to self. Absolute and relative searches are
|
106
|
+
# accepted. The potential absolute paths are determined by a Fixtury::PathResolver instance
|
107
|
+
# relative to this node's pathname.
|
108
|
+
#
|
109
|
+
# @param search [String] The search to be used for finding the node.
|
110
|
+
# @return [Fixtury::SchemaNode, NilClass] The node if found, `nil` otherwise.
|
111
|
+
# @raise [ArgumentError] if the search is blank.
|
112
|
+
# @alias []
|
113
|
+
def get(search)
|
114
|
+
raise ArgumentError, "`search` must be provided" if search.blank?
|
115
|
+
|
116
|
+
resolver = Fixtury::PathResolver.new(namespace: self.pathname, search: search)
|
117
|
+
resolver.possible_absolute_paths.each do |path|
|
118
|
+
target = first_ancestor
|
119
|
+
segments = path.split("/")
|
120
|
+
segments.reject!(&:blank?)
|
121
|
+
segments.shift if segments.first == target.name
|
122
|
+
segments.each do |segment|
|
123
|
+
target = target.children[segment]
|
124
|
+
break unless target
|
125
|
+
end
|
126
|
+
|
127
|
+
return target if target
|
128
|
+
end
|
129
|
+
|
130
|
+
nil
|
131
|
+
end
|
132
|
+
alias [] get
|
133
|
+
|
134
|
+
# Generates a string representing the structure of the schema tree.
|
135
|
+
# The string will be in the form of "type:name[isolation_key](options)". The children
|
136
|
+
# will be on the next line and indented by two spaces.
|
137
|
+
#
|
138
|
+
# @param prefix [String] The prefix to be used for any lines produced.
|
139
|
+
# @return [String] The structure string.
|
140
|
+
def structure(prefix = "")
|
141
|
+
out = []
|
142
|
+
|
143
|
+
opts = options.except(:isolate)
|
144
|
+
opts.compact!
|
145
|
+
|
146
|
+
my_structure = +"#{prefix}#{schema_node_type}:#{name}"
|
147
|
+
iso = isolation_key(default: nil)
|
148
|
+
my_structure << "[#{iso}]" if iso
|
149
|
+
my_structure << "(#{opts.to_a.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")})" if opts.present?
|
150
|
+
out << my_structure
|
151
|
+
|
152
|
+
children.each_value do |child|
|
153
|
+
out << child.structure("#{prefix} ")
|
154
|
+
end
|
155
|
+
out.join("\n")
|
156
|
+
end
|
157
|
+
|
158
|
+
# Applies options to the node and raises if a collision occurs.
|
159
|
+
# This is useful for reopening a node and ensuring options are not altered.
|
160
|
+
#
|
161
|
+
# @param opts [Hash] The options to apply to the node.
|
162
|
+
# @raise [Fixtury::Errors::OptionCollisionError] if the option is already set and the new value is different.
|
163
|
+
# @return [void]
|
164
|
+
def apply_options!(opts = {})
|
165
|
+
opts.each do |key, value|
|
166
|
+
if options.key?(key) && options[key] != value
|
167
|
+
raise Errors::OptionCollisionError.new(name, key, options[key], value)
|
168
|
+
end
|
169
|
+
|
170
|
+
options[key] = value
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
end
|
175
|
+
end
|