spec_forge 0.5.0 → 0.6.0
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/.standard.yml +3 -3
- data/CHANGELOG.md +106 -1
- data/README.md +34 -22
- data/flake.lock +3 -3
- data/flake.nix +8 -2
- data/lib/spec_forge/attribute/chainable.rb +208 -20
- data/lib/spec_forge/attribute/factory.rb +91 -14
- data/lib/spec_forge/attribute/faker.rb +62 -13
- data/lib/spec_forge/attribute/global.rb +96 -0
- data/lib/spec_forge/attribute/literal.rb +15 -2
- data/lib/spec_forge/attribute/matcher.rb +186 -11
- data/lib/spec_forge/attribute/parameterized.rb +45 -12
- data/lib/spec_forge/attribute/regex.rb +55 -5
- data/lib/spec_forge/attribute/resolvable.rb +48 -5
- data/lib/spec_forge/attribute/resolvable_array.rb +62 -4
- data/lib/spec_forge/attribute/resolvable_hash.rb +62 -4
- data/lib/spec_forge/attribute/store.rb +65 -0
- data/lib/spec_forge/attribute/transform.rb +33 -5
- data/lib/spec_forge/attribute/variable.rb +37 -6
- data/lib/spec_forge/attribute.rb +166 -66
- data/lib/spec_forge/backtrace_formatter.rb +26 -3
- data/lib/spec_forge/callbacks.rb +79 -0
- data/lib/spec_forge/cli/actions.rb +27 -0
- data/lib/spec_forge/cli/command.rb +78 -24
- data/lib/spec_forge/cli/init.rb +11 -1
- data/lib/spec_forge/cli/new.rb +54 -3
- data/lib/spec_forge/cli/run.rb +20 -0
- data/lib/spec_forge/cli.rb +16 -5
- data/lib/spec_forge/configuration.rb +94 -22
- data/lib/spec_forge/context/callbacks.rb +91 -0
- data/lib/spec_forge/context/global.rb +72 -0
- data/lib/spec_forge/context/store.rb +148 -0
- data/lib/spec_forge/context/variables.rb +91 -0
- data/lib/spec_forge/context.rb +36 -0
- data/lib/spec_forge/core_ext/rspec.rb +22 -4
- data/lib/spec_forge/error.rb +267 -113
- data/lib/spec_forge/factory.rb +33 -14
- data/lib/spec_forge/filter.rb +87 -0
- data/lib/spec_forge/forge.rb +170 -0
- data/lib/spec_forge/http/backend.rb +99 -29
- data/lib/spec_forge/http/client.rb +23 -13
- data/lib/spec_forge/http/request.rb +74 -62
- data/lib/spec_forge/http/verb.rb +79 -0
- data/lib/spec_forge/http.rb +105 -0
- data/lib/spec_forge/loader.rb +254 -0
- data/lib/spec_forge/matchers.rb +130 -0
- data/lib/spec_forge/normalizer/configuration.rb +24 -11
- data/lib/spec_forge/normalizer/constraint.rb +21 -8
- data/lib/spec_forge/normalizer/expectation.rb +31 -12
- data/lib/spec_forge/normalizer/factory.rb +24 -11
- data/lib/spec_forge/normalizer/factory_reference.rb +27 -13
- data/lib/spec_forge/normalizer/global_context.rb +88 -0
- data/lib/spec_forge/normalizer/spec.rb +39 -16
- data/lib/spec_forge/normalizer.rb +255 -41
- data/lib/spec_forge/runner/callbacks.rb +246 -0
- data/lib/spec_forge/runner/debug_proxy.rb +213 -0
- data/lib/spec_forge/runner/listener.rb +54 -0
- data/lib/spec_forge/runner/metadata.rb +58 -0
- data/lib/spec_forge/runner/state.rb +99 -0
- data/lib/spec_forge/runner.rb +132 -123
- data/lib/spec_forge/spec/expectation/constraint.rb +91 -20
- data/lib/spec_forge/spec/expectation.rb +43 -51
- data/lib/spec_forge/spec.rb +83 -96
- data/lib/spec_forge/type.rb +36 -4
- data/lib/spec_forge/version.rb +4 -1
- data/lib/spec_forge.rb +161 -76
- metadata +20 -5
- data/spec_forge/factories/user.yml +0 -4
- data/spec_forge/forge_helper.rb +0 -48
- data/spec_forge/specs/users.yml +0 -65
data/lib/spec_forge/attribute.rb
CHANGED
@@ -5,18 +5,21 @@ require_relative "attribute/parameterized"
|
|
5
5
|
require_relative "attribute/chainable"
|
6
6
|
require_relative "attribute/resolvable"
|
7
7
|
|
8
|
-
# Doesn't matter
|
9
|
-
require_relative "attribute/factory"
|
10
|
-
require_relative "attribute/faker"
|
11
|
-
require_relative "attribute/literal"
|
12
|
-
require_relative "attribute/matcher"
|
13
|
-
require_relative "attribute/regex"
|
14
|
-
require_relative "attribute/resolvable_array"
|
15
|
-
require_relative "attribute/resolvable_hash"
|
16
|
-
require_relative "attribute/transform"
|
17
|
-
require_relative "attribute/variable"
|
18
|
-
|
19
8
|
module SpecForge
|
9
|
+
#
|
10
|
+
# Base class for all attribute types in SpecForge.
|
11
|
+
# Attributes represent values that can be transformed, resolved, or have special meaning
|
12
|
+
# in the context of specs and expectations.
|
13
|
+
#
|
14
|
+
# The Attribute system handles dynamic data generation, variable references,
|
15
|
+
# matchers, transformations and other special values in YAML specs.
|
16
|
+
#
|
17
|
+
# @example Basic usage in YAML
|
18
|
+
# username: faker.internet.username # A dynamic faker attribute
|
19
|
+
# email: /\w+@\w+\.\w+/ # A regex attribute
|
20
|
+
# status: kind_of.integer # A matcher attribute
|
21
|
+
# user_id: variables.user.id # A variable reference
|
22
|
+
#
|
20
23
|
class Attribute
|
21
24
|
include Resolvable
|
22
25
|
|
@@ -66,7 +69,7 @@ module SpecForge
|
|
66
69
|
end
|
67
70
|
|
68
71
|
#
|
69
|
-
# Creates an Attribute instance from a string
|
72
|
+
# Creates an Attribute instance from a string
|
70
73
|
#
|
71
74
|
# @param string [String] The input string
|
72
75
|
#
|
@@ -75,24 +78,31 @@ module SpecForge
|
|
75
78
|
# @private
|
76
79
|
#
|
77
80
|
def self.from_string(string)
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
Regex
|
89
|
-
|
90
|
-
|
91
|
-
|
81
|
+
klass =
|
82
|
+
case string
|
83
|
+
when Factory::KEYWORD_REGEX
|
84
|
+
Factory
|
85
|
+
when Faker::KEYWORD_REGEX
|
86
|
+
Faker
|
87
|
+
when Global::KEYWORD_REGEX
|
88
|
+
Global
|
89
|
+
when Matcher::KEYWORD_REGEX
|
90
|
+
Matcher
|
91
|
+
when Regex::KEYWORD_REGEX
|
92
|
+
Regex
|
93
|
+
when Store::KEYWORD_REGEX
|
94
|
+
Store
|
95
|
+
when Variable::KEYWORD_REGEX
|
96
|
+
Variable
|
97
|
+
else
|
98
|
+
Literal
|
99
|
+
end
|
100
|
+
|
101
|
+
klass.new(string)
|
92
102
|
end
|
93
103
|
|
94
104
|
#
|
95
|
-
# Creates an Attribute instance from a hash
|
105
|
+
# Creates an Attribute instance from a hash
|
96
106
|
#
|
97
107
|
# @param hash [Hash] The input hash
|
98
108
|
#
|
@@ -118,66 +128,156 @@ module SpecForge
|
|
118
128
|
end
|
119
129
|
end
|
120
130
|
|
131
|
+
#
|
132
|
+
# The original input value
|
133
|
+
#
|
134
|
+
# @return [Object]
|
135
|
+
#
|
121
136
|
attr_reader :input
|
122
137
|
|
123
138
|
#
|
124
|
-
#
|
139
|
+
# Creates a new attribute
|
140
|
+
#
|
141
|
+
# @param input [Object] The original input value
|
125
142
|
#
|
126
143
|
def initialize(input)
|
127
144
|
@input = input
|
128
145
|
end
|
129
146
|
|
130
147
|
#
|
131
|
-
#
|
148
|
+
# Compares this attributes input to other
|
149
|
+
#
|
150
|
+
# @param other [Object, Attribute] If another Attribute, the input will be compared
|
151
|
+
#
|
152
|
+
# @return [Boolean]
|
153
|
+
#
|
154
|
+
def ==(other)
|
155
|
+
other =
|
156
|
+
if other.is_a?(Attribute)
|
157
|
+
other.input
|
158
|
+
else
|
159
|
+
other
|
160
|
+
end
|
161
|
+
|
162
|
+
input == other
|
163
|
+
end
|
164
|
+
|
165
|
+
#
|
166
|
+
# Returns the processed value of this attribute.
|
167
|
+
# Recursively calls #value on underlying attributes, but does NOT resolve
|
168
|
+
# all nested structures completely.
|
132
169
|
#
|
133
|
-
#
|
134
|
-
# For generated values (Faker, Transform), this is the result of their operations.
|
170
|
+
# This returns an intermediate representation - for fully resolved values, use #resolve instead.
|
135
171
|
#
|
136
172
|
# @return [Object] The processed value of this attribute
|
137
173
|
#
|
138
174
|
# @raise [RuntimeError] if not implemented by subclass
|
139
175
|
#
|
176
|
+
# @example
|
177
|
+
# variable_attr = Attribute::Variable.new("variables.user")
|
178
|
+
# variable_attr.value # => User instance, but any attributes of User remain
|
179
|
+
# as Attribute objects
|
180
|
+
#
|
140
181
|
def value
|
141
182
|
raise "not implemented"
|
142
183
|
end
|
143
184
|
|
144
185
|
#
|
145
|
-
# Returns the fully evaluated result
|
186
|
+
# Returns the fully evaluated result with complete recursive resolution.
|
187
|
+
# Calls #value internally and then resolves all nested attributes, caching the result.
|
146
188
|
#
|
147
|
-
#
|
189
|
+
# Use this when you need the final, fully-resolved value with all nested attributes
|
190
|
+
# fully evaluated to their primitive values.
|
148
191
|
#
|
149
|
-
# @
|
150
|
-
# attr = Attribute::Literal.new("hello")
|
151
|
-
# attr.resolve # => "hello"
|
192
|
+
# @return [Object] The completely resolved value with cached results
|
152
193
|
#
|
153
|
-
# @example
|
154
|
-
#
|
155
|
-
#
|
194
|
+
# @example
|
195
|
+
# faker_attr = Attribute::Faker.new("faker.name.first_name")
|
196
|
+
# faker_attr.resolved # => "Jane" (result is cached in @resolved)
|
197
|
+
# faker_attr.resolved # => "Jane" (returns same cached value)
|
156
198
|
#
|
157
|
-
def
|
158
|
-
@resolved ||=
|
199
|
+
def resolved
|
200
|
+
@resolved ||= resolve
|
159
201
|
end
|
160
202
|
|
161
|
-
|
162
|
-
|
203
|
+
#
|
204
|
+
# Performs recursive resolution of the attribute's value.
|
205
|
+
# Handles nested arrays and hashes by recursively resolving their elements.
|
206
|
+
#
|
207
|
+
# Unlike #resolved, this method doesn't cache results and can be used
|
208
|
+
# when fresh resolution is needed each time.
|
209
|
+
#
|
210
|
+
# @return [Object] The recursively resolved value without caching
|
211
|
+
#
|
212
|
+
# @example
|
213
|
+
# hash_attr = Attribute::ResolvableHash.new({name: Attribute::Faker.new("faker.name.name")})
|
214
|
+
# hash_attr.resolve # => {name: "John Smith"}
|
215
|
+
# hash_attr.resolve # => {name: "Jane Doe"} (different value on each call)
|
216
|
+
#
|
217
|
+
def resolve
|
218
|
+
case value
|
219
|
+
when ArrayLike
|
220
|
+
value.map(&resolved_proc)
|
221
|
+
when HashLike
|
222
|
+
value.transform_values(&resolved_proc)
|
223
|
+
else
|
224
|
+
value
|
225
|
+
end
|
163
226
|
end
|
164
227
|
|
165
228
|
#
|
166
|
-
#
|
229
|
+
# Converts this attribute to an appropriate RSpec matcher.
|
230
|
+
# Handles different types of values by creating the right matcher type:
|
231
|
+
# - Arrays become contain_exactly matchers
|
232
|
+
# - Hashes become include matchers
|
233
|
+
# - Regexp become match matchers
|
234
|
+
# - Existing matchers are passed through
|
235
|
+
# - Other values become eq matchers
|
167
236
|
#
|
168
|
-
#
|
237
|
+
# This method is crucial for nested matcher structures and compound matchers
|
238
|
+
# like matcher.and that require all values to be proper matchers.
|
169
239
|
#
|
170
|
-
# @return [
|
240
|
+
# @return [RSpec::Matchers::BuiltIn::BaseMatcher] A matcher representing this attribute
|
171
241
|
#
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
242
|
+
# @example Converting different values to matchers
|
243
|
+
# literal_attr = Attribute::Literal.new("hello")
|
244
|
+
# literal_attr.resolve_as_matcher # => eq("hello")
|
245
|
+
#
|
246
|
+
# array_attr = Attribute::ResolvableArray.new([1, 2, 3])
|
247
|
+
# array_attr.resolve_as_matcher # => contain_exactly(eq(1), eq(2), eq(3))
|
248
|
+
#
|
249
|
+
# hash_attr = Attribute::ResolvableHash.new({name: "Test"})
|
250
|
+
# hash_attr.resolve_as_matcher # => include("name" => eq("Test"))
|
251
|
+
#
|
252
|
+
def resolve_as_matcher
|
253
|
+
methods = Attribute::Matcher::MATCHER_METHODS
|
254
|
+
|
255
|
+
case resolved
|
256
|
+
when Array, ArrayLike
|
257
|
+
resolved_array = resolved.map(&resolve_as_matcher_proc)
|
258
|
+
|
259
|
+
if resolved_array.size > 0
|
260
|
+
methods.contain_exactly(*resolved_array)
|
176
261
|
else
|
177
|
-
|
262
|
+
methods.eq([])
|
178
263
|
end
|
264
|
+
when Hash, HashLike
|
265
|
+
resolved_hash = resolved.transform_values(&resolve_as_matcher_proc).stringify_keys
|
179
266
|
|
180
|
-
|
267
|
+
if resolved_hash.size > 0
|
268
|
+
methods.include(**resolved_hash)
|
269
|
+
else
|
270
|
+
methods.eq({})
|
271
|
+
end
|
272
|
+
when Attribute::Matcher, Regexp
|
273
|
+
methods.match(resolved)
|
274
|
+
when RSpec::Matchers::BuiltIn::BaseMatcher,
|
275
|
+
RSpec::Matchers::DSL::Matcher,
|
276
|
+
Class
|
277
|
+
resolved # Pass through
|
278
|
+
else
|
279
|
+
methods.eq(resolved)
|
280
|
+
end
|
181
281
|
end
|
182
282
|
|
183
283
|
#
|
@@ -185,20 +285,20 @@ module SpecForge
|
|
185
285
|
#
|
186
286
|
# @param variables [Hash] A hash of variable attributes
|
187
287
|
#
|
188
|
-
def bind_variables(
|
189
|
-
end
|
190
|
-
|
191
|
-
protected
|
192
|
-
|
193
|
-
def __resolve(value)
|
194
|
-
case value
|
195
|
-
when ArrayLike
|
196
|
-
value.map(&resolvable_proc)
|
197
|
-
when HashLike
|
198
|
-
value.transform_values(&resolvable_proc)
|
199
|
-
else
|
200
|
-
value
|
201
|
-
end
|
288
|
+
def bind_variables(variables)
|
202
289
|
end
|
203
290
|
end
|
204
291
|
end
|
292
|
+
|
293
|
+
# Order doesn't matter
|
294
|
+
require_relative "attribute/factory"
|
295
|
+
require_relative "attribute/faker"
|
296
|
+
require_relative "attribute/global"
|
297
|
+
require_relative "attribute/literal"
|
298
|
+
require_relative "attribute/matcher"
|
299
|
+
require_relative "attribute/regex"
|
300
|
+
require_relative "attribute/resolvable_array"
|
301
|
+
require_relative "attribute/resolvable_hash"
|
302
|
+
require_relative "attribute/store"
|
303
|
+
require_relative "attribute/transform"
|
304
|
+
require_relative "attribute/variable"
|
@@ -2,18 +2,41 @@
|
|
2
2
|
|
3
3
|
module SpecForge
|
4
4
|
#
|
5
|
-
# Used internally by RSpec
|
6
|
-
#
|
5
|
+
# Used internally by RSpec to format backtraces for test failures
|
6
|
+
# Customizes error output to make it more readable and useful for SpecForge
|
7
7
|
#
|
8
8
|
module BacktraceFormatter
|
9
|
+
#
|
10
|
+
# Returns the RSpec backtrace formatter instance
|
11
|
+
# Lazily initializes the formatter on first access
|
12
|
+
#
|
13
|
+
# @return [RSpec::Core::BacktraceFormatter] The backtrace formatter
|
14
|
+
#
|
9
15
|
def self.formatter
|
10
16
|
@formatter ||= RSpec::Core::BacktraceFormatter.new
|
11
17
|
end
|
12
18
|
|
19
|
+
#
|
20
|
+
# Formats a single backtrace line
|
21
|
+
# Delegates to the RSpec formatter
|
22
|
+
#
|
23
|
+
# @param line [String] The backtrace line to format
|
24
|
+
#
|
25
|
+
# @return [String] The formatted backtrace line
|
26
|
+
#
|
13
27
|
def self.backtrace_line(line)
|
14
28
|
formatter.backtrace_line(line)
|
15
29
|
end
|
16
30
|
|
31
|
+
#
|
32
|
+
# Formats a complete backtrace for an example
|
33
|
+
# Adds the YAML location to the front of the backtrace for better context
|
34
|
+
#
|
35
|
+
# @param backtrace [Array<String>] The raw backtrace lines
|
36
|
+
# @param example_metadata [Hash] Metadata about the failing example
|
37
|
+
#
|
38
|
+
# @return [Array<String>] The formatted backtrace with YAML location first
|
39
|
+
#
|
17
40
|
def self.format_backtrace(backtrace, example_metadata)
|
18
41
|
backtrace = SpecForge.backtrace_cleaner.clean(backtrace)
|
19
42
|
|
@@ -21,7 +44,7 @@ module SpecForge
|
|
21
44
|
line_number = example_metadata[:example_group][:line_number]
|
22
45
|
|
23
46
|
# Add the yaml location to the front so it's the first thing people see
|
24
|
-
["#{location}:#{line_number}"] + backtrace
|
47
|
+
["#{location}:#{line_number}"] + backtrace[0..50]
|
25
48
|
end
|
26
49
|
end
|
27
50
|
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
#
|
5
|
+
# Manages user-defined callbacks for test lifecycle events
|
6
|
+
#
|
7
|
+
# This singleton class stores and executes callback functions that
|
8
|
+
# users can register to run at specific points in the test lifecycle.
|
9
|
+
# Each callback receives a context object containing relevant state
|
10
|
+
# information for that point in execution.
|
11
|
+
#
|
12
|
+
# @example Registering and using a callback
|
13
|
+
# SpecForge::Callbacks.register(:my_callback) do |context|
|
14
|
+
# puts "Running test: #{context.expectation_name}"
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
class Callbacks < Hash
|
18
|
+
include Singleton
|
19
|
+
|
20
|
+
class << self
|
21
|
+
#
|
22
|
+
# Registers a new callback for a specific event
|
23
|
+
#
|
24
|
+
# @param name [String, Symbol] The name of the callback event
|
25
|
+
# @param block [Proc] The callback function to execute
|
26
|
+
#
|
27
|
+
# @raise [ArgumentError] If no block is provided
|
28
|
+
#
|
29
|
+
def register(name, &block)
|
30
|
+
raise ArgumentError, "A block must be provided" unless block.is_a?(Proc)
|
31
|
+
|
32
|
+
if registered?(name)
|
33
|
+
warn("Callback #{name.in_quotes} is already registered. It will be overwritten")
|
34
|
+
end
|
35
|
+
|
36
|
+
instance[name.to_s] = block
|
37
|
+
end
|
38
|
+
|
39
|
+
#
|
40
|
+
# Checks if a callback is registered for the given event
|
41
|
+
#
|
42
|
+
# @param name [String, Symbol] The name of the callback event
|
43
|
+
#
|
44
|
+
# @return [Boolean] True if the callback exists
|
45
|
+
#
|
46
|
+
def registered?(name)
|
47
|
+
instance.key?(name.to_s)
|
48
|
+
end
|
49
|
+
|
50
|
+
#
|
51
|
+
# Returns all registered callback names
|
52
|
+
#
|
53
|
+
# @return [Array<String>] List of registered callback names
|
54
|
+
#
|
55
|
+
def registered_names
|
56
|
+
instance.keys
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Executes a named callback with the provided context
|
61
|
+
#
|
62
|
+
# @param name [String, Symbol] The name of the callback to run
|
63
|
+
# @param context [Object] Context object containing state data
|
64
|
+
#
|
65
|
+
# @raise [ArgumentError] If the callback is not registered
|
66
|
+
#
|
67
|
+
def run(name, context)
|
68
|
+
callback = instance[name.to_s]
|
69
|
+
raise ArgumentError, "Callback #{name.in_quotes} is not defined" if callback.nil?
|
70
|
+
|
71
|
+
if callback.arity == 0
|
72
|
+
callback.call
|
73
|
+
else
|
74
|
+
callback.call(context)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -2,17 +2,44 @@
|
|
2
2
|
|
3
3
|
module SpecForge
|
4
4
|
class CLI
|
5
|
+
#
|
6
|
+
# Provides helper methods for CLI actions such as file generation
|
7
|
+
# and template rendering through Thor::Actions integration.
|
8
|
+
#
|
9
|
+
# @example Using actions in a command
|
10
|
+
# actions.template("my_template.tt", "destination/path.rb")
|
11
|
+
#
|
5
12
|
module Actions
|
13
|
+
#
|
14
|
+
# Internal Ruby hook, called when the module is included in another file
|
15
|
+
#
|
16
|
+
# @param base [Class] The class that included this module
|
17
|
+
#
|
6
18
|
def self.included(base)
|
19
|
+
#
|
20
|
+
# Returns an ActionContext instance for performing file operations
|
21
|
+
#
|
22
|
+
# @return [ActionContext] The action context for this command
|
23
|
+
#
|
7
24
|
base.define_method(:actions) do
|
8
25
|
@actions ||= ActionContext.new
|
9
26
|
end
|
10
27
|
end
|
11
28
|
end
|
12
29
|
|
30
|
+
#
|
31
|
+
# Provides a context for Thor actions that configures paths and options
|
32
|
+
#
|
33
|
+
# @private
|
34
|
+
#
|
13
35
|
class ActionContext < Thor
|
14
36
|
include Thor::Actions
|
15
37
|
|
38
|
+
#
|
39
|
+
# Creates a new action context with SpecForge template paths configured
|
40
|
+
#
|
41
|
+
# @return [ActionContext] A new context for Thor actions
|
42
|
+
#
|
16
43
|
def initialize(...)
|
17
44
|
self.class.source_root(File.expand_path("../../templates", __dir__))
|
18
45
|
self.destination_root = SpecForge.root
|
@@ -2,20 +2,55 @@
|
|
2
2
|
|
3
3
|
module SpecForge
|
4
4
|
class CLI
|
5
|
+
#
|
6
|
+
# Base class for CLI commands that provides common functionality and
|
7
|
+
# defines the DSL for declaring command properties.
|
8
|
+
#
|
9
|
+
# @example Defining a simple command
|
10
|
+
# class MyCommand < Command
|
11
|
+
# command_name "my_command"
|
12
|
+
# syntax "my_command [options]"
|
13
|
+
# summary "Does something awesome"
|
14
|
+
# description "A longer description of what this command does"
|
15
|
+
#
|
16
|
+
# option "-f", "--force", "Force the operation"
|
17
|
+
#
|
18
|
+
# def call
|
19
|
+
# # Command implementation
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
5
23
|
class Command
|
6
24
|
include CLI::Actions
|
7
25
|
|
8
26
|
class << self
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
27
|
+
#
|
28
|
+
# Sets the command's name
|
29
|
+
#
|
30
|
+
attr_writer :command_name
|
31
|
+
|
32
|
+
#
|
33
|
+
# Sets the command's syntax string
|
34
|
+
#
|
35
|
+
attr_writer :syntax
|
36
|
+
|
37
|
+
#
|
38
|
+
# Sets the command's detailed description
|
39
|
+
#
|
40
|
+
attr_writer :description
|
41
|
+
|
42
|
+
#
|
43
|
+
# Sets a brief summary of the command
|
44
|
+
#
|
45
|
+
attr_writer :summary
|
46
|
+
|
47
|
+
#
|
48
|
+
# Sets the command's available options
|
49
|
+
#
|
50
|
+
attr_writer :options
|
16
51
|
|
17
52
|
#
|
18
|
-
#
|
53
|
+
# Sets the command's name
|
19
54
|
#
|
20
55
|
# @param name [String] The name of the command
|
21
56
|
#
|
@@ -24,37 +59,37 @@ module SpecForge
|
|
24
59
|
end
|
25
60
|
|
26
61
|
#
|
27
|
-
#
|
62
|
+
# Sets the command's syntax
|
28
63
|
#
|
29
|
-
# @param syntax [String]
|
64
|
+
# @param syntax [String] The command syntax to display in help
|
30
65
|
#
|
31
66
|
def syntax(syntax)
|
32
67
|
self.syntax = syntax
|
33
68
|
end
|
34
69
|
|
35
70
|
#
|
36
|
-
#
|
71
|
+
# Sets the command's description, displayed in detailed help
|
37
72
|
#
|
38
|
-
# @param description [String]
|
73
|
+
# @param description [String] The detailed command description
|
39
74
|
#
|
40
75
|
def description(description)
|
41
76
|
self.description = description
|
42
77
|
end
|
43
78
|
|
44
79
|
#
|
45
|
-
#
|
80
|
+
# Sets the command's summary, displayed in command list
|
46
81
|
#
|
47
|
-
# @param summary [String]
|
82
|
+
# @param summary [String] The short command summary
|
48
83
|
#
|
49
84
|
def summary(summary)
|
50
85
|
self.summary = summary
|
51
86
|
end
|
52
87
|
|
53
88
|
#
|
54
|
-
#
|
89
|
+
# Adds an example of how to use the command
|
55
90
|
#
|
56
|
-
# @param command [String] The example
|
57
|
-
# @param description [String] Description of the example
|
91
|
+
# @param command [String] The example command
|
92
|
+
# @param description [String] Description of what the example does
|
58
93
|
#
|
59
94
|
def example(command, description)
|
60
95
|
@examples ||= []
|
@@ -64,7 +99,10 @@ module SpecForge
|
|
64
99
|
end
|
65
100
|
|
66
101
|
#
|
67
|
-
#
|
102
|
+
# Adds a command line option
|
103
|
+
#
|
104
|
+
# @param args [Array<String>] The option flags (e.g., "-f", "--force")
|
105
|
+
# @yield [value] Block to handle the option value
|
68
106
|
#
|
69
107
|
def option(*args, &block)
|
70
108
|
@options ||= []
|
@@ -73,9 +111,9 @@ module SpecForge
|
|
73
111
|
end
|
74
112
|
|
75
113
|
#
|
76
|
-
#
|
114
|
+
# Adds command aliases
|
77
115
|
#
|
78
|
-
# @param
|
116
|
+
# @param aliases [Array<String>] Alias names for this command
|
79
117
|
#
|
80
118
|
def aliases(*aliases)
|
81
119
|
@aliases ||= []
|
@@ -86,7 +124,7 @@ module SpecForge
|
|
86
124
|
#
|
87
125
|
# Registers the command with Commander
|
88
126
|
#
|
89
|
-
# @param context [Commander::Command]
|
127
|
+
# @param context [Commander::Command] The Commander context
|
90
128
|
#
|
91
129
|
# @private
|
92
130
|
#
|
@@ -112,11 +150,27 @@ module SpecForge
|
|
112
150
|
end
|
113
151
|
end
|
114
152
|
|
115
|
-
|
153
|
+
#
|
154
|
+
# Command arguments passed from the command line
|
155
|
+
#
|
156
|
+
# @return [Array] The positional arguments
|
157
|
+
#
|
158
|
+
attr_reader :arguments
|
159
|
+
|
160
|
+
#
|
161
|
+
# Command options passed from the command line
|
162
|
+
#
|
163
|
+
# @return [Hash] The flag arguments
|
164
|
+
#
|
165
|
+
attr_reader :options
|
116
166
|
|
117
167
|
#
|
118
|
-
#
|
119
|
-
#
|
168
|
+
# Creates a new command instance
|
169
|
+
#
|
170
|
+
# @param arguments [Array] Any positional arguments from the command line
|
171
|
+
# @param options [Hash] Any flag arguments from the command line
|
172
|
+
#
|
173
|
+
# @return [Command] A new command instance
|
120
174
|
#
|
121
175
|
def initialize(arguments, options)
|
122
176
|
@arguments = arguments
|
data/lib/spec_forge/cli/init.rb
CHANGED
@@ -2,13 +2,23 @@
|
|
2
2
|
|
3
3
|
module SpecForge
|
4
4
|
class CLI
|
5
|
+
#
|
6
|
+
# Command for initializing a new SpecForge project structure
|
7
|
+
#
|
8
|
+
# @example Creating a new SpecForge project
|
9
|
+
# spec_forge init
|
10
|
+
#
|
5
11
|
class Init < Command
|
6
12
|
command_name "init"
|
7
13
|
syntax "init"
|
8
14
|
summary "Initializes directory structure and configuration files"
|
9
15
|
|
16
|
+
#
|
17
|
+
# Creates the "spec_forge", "spec_forge/factories", and "spec_forge/specs" directories
|
18
|
+
# Also creates the "spec_forge.rb" initialization file
|
19
|
+
#
|
10
20
|
def call
|
11
|
-
base_path = SpecForge.
|
21
|
+
base_path = SpecForge.forge_path
|
12
22
|
actions.empty_directory "#{base_path}/factories"
|
13
23
|
actions.empty_directory "#{base_path}/specs"
|
14
24
|
|