fixtury 0.4.1 → 1.0.0.beta2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|