spec_forge 0.1.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 +7 -0
- data/.envrc +1 -0
- data/.rspec +1 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +426 -0
- data/Rakefile +6 -0
- data/bin/spec_forge +5 -0
- data/flake.lock +61 -0
- data/flake.nix +41 -0
- data/lib/spec_forge/attribute/chainable.rb +86 -0
- data/lib/spec_forge/attribute/factory.rb +63 -0
- data/lib/spec_forge/attribute/faker.rb +54 -0
- data/lib/spec_forge/attribute/literal.rb +27 -0
- data/lib/spec_forge/attribute/matcher.rb +118 -0
- data/lib/spec_forge/attribute/parameterized.rb +76 -0
- data/lib/spec_forge/attribute/resolvable.rb +21 -0
- data/lib/spec_forge/attribute/resolvable_array.rb +24 -0
- data/lib/spec_forge/attribute/resolvable_hash.rb +24 -0
- data/lib/spec_forge/attribute/transform.rb +39 -0
- data/lib/spec_forge/attribute/variable.rb +36 -0
- data/lib/spec_forge/attribute.rb +208 -0
- data/lib/spec_forge/cli/actions.rb +23 -0
- data/lib/spec_forge/cli/command.rb +127 -0
- data/lib/spec_forge/cli/init.rb +29 -0
- data/lib/spec_forge/cli/new.rb +161 -0
- data/lib/spec_forge/cli/run.rb +17 -0
- data/lib/spec_forge/cli.rb +43 -0
- data/lib/spec_forge/config.rb +84 -0
- data/lib/spec_forge/environment.rb +71 -0
- data/lib/spec_forge/error.rb +150 -0
- data/lib/spec_forge/factory.rb +104 -0
- data/lib/spec_forge/http/backend.rb +106 -0
- data/lib/spec_forge/http/client.rb +33 -0
- data/lib/spec_forge/http/request.rb +93 -0
- data/lib/spec_forge/http/verb.rb +118 -0
- data/lib/spec_forge/http.rb +6 -0
- data/lib/spec_forge/normalizer/config.rb +104 -0
- data/lib/spec_forge/normalizer/constraint.rb +47 -0
- data/lib/spec_forge/normalizer/expectation.rb +85 -0
- data/lib/spec_forge/normalizer/factory.rb +65 -0
- data/lib/spec_forge/normalizer/factory_reference.rb +66 -0
- data/lib/spec_forge/normalizer/spec.rb +73 -0
- data/lib/spec_forge/normalizer.rb +183 -0
- data/lib/spec_forge/runner.rb +91 -0
- data/lib/spec_forge/spec/expectation/constraint.rb +52 -0
- data/lib/spec_forge/spec/expectation.rb +53 -0
- data/lib/spec_forge/spec.rb +77 -0
- data/lib/spec_forge/type.rb +45 -0
- data/lib/spec_forge/version.rb +5 -0
- data/lib/spec_forge.rb +90 -0
- data/lib/templates/config.tt +19 -0
- data/spec_forge/config.yml +19 -0
- data/spec_forge/factories/user.yml +4 -0
- data/spec_forge/specs/users.yml +63 -0
- metadata +234 -0
@@ -0,0 +1,208 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Need to be first
|
4
|
+
require_relative "attribute/parameterized"
|
5
|
+
require_relative "attribute/chainable"
|
6
|
+
require_relative "attribute/resolvable"
|
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/resolvable_array"
|
14
|
+
require_relative "attribute/resolvable_hash"
|
15
|
+
require_relative "attribute/transform"
|
16
|
+
require_relative "attribute/variable"
|
17
|
+
|
18
|
+
module SpecForge
|
19
|
+
class Attribute
|
20
|
+
#
|
21
|
+
# Binds variables to Attribute objects
|
22
|
+
#
|
23
|
+
# @param input [Array, Hash, Attribute] The input to loop through or bind to
|
24
|
+
# @param variables [Hash] Any variables to available to assign
|
25
|
+
#
|
26
|
+
# @return [Array, Hash, Attribute] The input with bounded variables
|
27
|
+
#
|
28
|
+
def self.bind_variables(input, variables = {})
|
29
|
+
case input
|
30
|
+
when ArrayLike
|
31
|
+
input.each { |v| v.bind_variables(variables) }
|
32
|
+
when HashLike
|
33
|
+
input.each_value { |v| v.bind_variables(variables) }
|
34
|
+
when Attribute
|
35
|
+
input.bind_variables(variables)
|
36
|
+
end
|
37
|
+
|
38
|
+
input
|
39
|
+
end
|
40
|
+
|
41
|
+
#
|
42
|
+
# Creates an Attribute instance based on the input value's type and content.
|
43
|
+
# Recursively converts Array and Hash
|
44
|
+
#
|
45
|
+
# @param value [Object] The input value to convert into an Attribute
|
46
|
+
#
|
47
|
+
# @return [Attribute] A new Attribute instance of the appropriate subclass
|
48
|
+
#
|
49
|
+
def self.from(value)
|
50
|
+
case value
|
51
|
+
when String
|
52
|
+
from_string(value)
|
53
|
+
when HashLike
|
54
|
+
from_hash(value)
|
55
|
+
when Attribute
|
56
|
+
value
|
57
|
+
when ArrayLike
|
58
|
+
array = value.map { |v| Attribute.from(v) }
|
59
|
+
Attribute::ResolvableArray.new(array)
|
60
|
+
else
|
61
|
+
Literal.new(value)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
#
|
66
|
+
# Creates an Attribute instance from a string, handling any macros
|
67
|
+
#
|
68
|
+
# @param string [String] The input string
|
69
|
+
#
|
70
|
+
# @return [Attribute]
|
71
|
+
#
|
72
|
+
# @private
|
73
|
+
#
|
74
|
+
def self.from_string(string)
|
75
|
+
case string
|
76
|
+
when Faker::KEYWORD_REGEX
|
77
|
+
Faker.new(string)
|
78
|
+
when Variable::KEYWORD_REGEX
|
79
|
+
Variable.new(string)
|
80
|
+
when Matcher::KEYWORD_REGEX
|
81
|
+
Matcher.new(string)
|
82
|
+
when Factory::KEYWORD_REGEX
|
83
|
+
Factory.new(string)
|
84
|
+
else
|
85
|
+
Literal.new(string)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
#
|
90
|
+
# Creates an Attribute instance from a hash, handling any macros
|
91
|
+
#
|
92
|
+
# @param hash [Hash] The input hash
|
93
|
+
#
|
94
|
+
# @return [Attribute]
|
95
|
+
#
|
96
|
+
# @private
|
97
|
+
#
|
98
|
+
def self.from_hash(hash)
|
99
|
+
# Determine if the hash is an expanded macro call
|
100
|
+
has_macro = ->(h, regex) { h.any? { |k, _| k.match?(regex) } }
|
101
|
+
|
102
|
+
if has_macro.call(hash, Transform::KEYWORD_REGEX)
|
103
|
+
Transform.from_hash(hash)
|
104
|
+
elsif has_macro.call(hash, Faker::KEYWORD_REGEX)
|
105
|
+
Faker.from_hash(hash)
|
106
|
+
elsif has_macro.call(hash, Matcher::KEYWORD_REGEX)
|
107
|
+
Matcher.from_hash(hash)
|
108
|
+
elsif has_macro.call(hash, Factory::KEYWORD_REGEX)
|
109
|
+
Factory.from_hash(hash)
|
110
|
+
else
|
111
|
+
hash = hash.transform_values { |v| Attribute.from(v) }
|
112
|
+
Attribute::ResolvableHash.new(hash)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
attr_reader :input
|
117
|
+
|
118
|
+
#
|
119
|
+
# @param input [Object] Anything
|
120
|
+
#
|
121
|
+
def initialize(input)
|
122
|
+
@input = input
|
123
|
+
end
|
124
|
+
|
125
|
+
#
|
126
|
+
# Returns the processed value of the input
|
127
|
+
#
|
128
|
+
# For literals, this is the input itself.
|
129
|
+
# For generated values (Faker, Transform), this is the result of their operations.
|
130
|
+
#
|
131
|
+
# @return [Object] The processed value of this attribute
|
132
|
+
#
|
133
|
+
# @raise [RuntimeError] if not implemented by subclass
|
134
|
+
#
|
135
|
+
def value
|
136
|
+
raise "not implemented"
|
137
|
+
end
|
138
|
+
|
139
|
+
#
|
140
|
+
# Returns the fully evaluated result, recursively resolving any nested attributes
|
141
|
+
# Note: This method can only be called once to ensure data is correct across the board
|
142
|
+
# You can still call #value if you need a new value
|
143
|
+
#
|
144
|
+
# @return [Object] The resolved value
|
145
|
+
#
|
146
|
+
# @example Simple literal
|
147
|
+
# attr = Attribute::Literal.new("hello")
|
148
|
+
# attr.resolve # => "hello"
|
149
|
+
#
|
150
|
+
# @example Nested array with faker
|
151
|
+
# attr = Attribute::Literal.new(["faker.number.positive", ["faker.name.first_name"]])
|
152
|
+
# attr.resolve # => [42, ["Jane"]]
|
153
|
+
#
|
154
|
+
def resolve
|
155
|
+
# Past test for the variable
|
156
|
+
@resolved ||= __resolve(value)
|
157
|
+
end
|
158
|
+
|
159
|
+
#
|
160
|
+
# Wraps the call to #resolve in a proc. Used with FactoryBot
|
161
|
+
#
|
162
|
+
# @return [Proc]
|
163
|
+
#
|
164
|
+
def to_proc
|
165
|
+
this = self # kek - what are we, javascript?
|
166
|
+
-> { this.resolve }
|
167
|
+
end
|
168
|
+
|
169
|
+
#
|
170
|
+
# Compares this attributes input to other
|
171
|
+
#
|
172
|
+
# @param other [Object, Attribute] If another Attribute, the input will be compared
|
173
|
+
#
|
174
|
+
# @return [Boolean]
|
175
|
+
#
|
176
|
+
def ==(other)
|
177
|
+
other =
|
178
|
+
if other.is_a?(Attribute)
|
179
|
+
other.input
|
180
|
+
else
|
181
|
+
other
|
182
|
+
end
|
183
|
+
|
184
|
+
input == other
|
185
|
+
end
|
186
|
+
|
187
|
+
#
|
188
|
+
# Used to bind variables to self or any sub attributes
|
189
|
+
#
|
190
|
+
# @param variables [Hash] A hash of variable attributes
|
191
|
+
#
|
192
|
+
def bind_variables(_variables)
|
193
|
+
end
|
194
|
+
|
195
|
+
protected
|
196
|
+
|
197
|
+
def __resolve(value)
|
198
|
+
case value
|
199
|
+
when ArrayLike
|
200
|
+
value.map(&:resolve)
|
201
|
+
when HashLike
|
202
|
+
value.transform_values(&:resolve)
|
203
|
+
else
|
204
|
+
value
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class CLI
|
5
|
+
module Actions
|
6
|
+
def self.included(base)
|
7
|
+
base.define_method(:actions) do
|
8
|
+
@actions ||= ActionContext.new
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class ActionContext < Thor
|
14
|
+
include Thor::Actions
|
15
|
+
|
16
|
+
def initialize(...)
|
17
|
+
self.class.source_root(File.expand_path("../../templates", __dir__))
|
18
|
+
self.destination_root = SpecForge.root
|
19
|
+
self.options = {}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class CLI
|
5
|
+
class Command
|
6
|
+
include CLI::Actions
|
7
|
+
|
8
|
+
class << self
|
9
|
+
attr_writer(*%i[
|
10
|
+
command_name
|
11
|
+
syntax
|
12
|
+
description
|
13
|
+
summary
|
14
|
+
options
|
15
|
+
])
|
16
|
+
|
17
|
+
#
|
18
|
+
# The command's name
|
19
|
+
#
|
20
|
+
# @param name [String] The name of the command
|
21
|
+
#
|
22
|
+
def command_name(name)
|
23
|
+
self.command_name = name
|
24
|
+
end
|
25
|
+
|
26
|
+
#
|
27
|
+
# The command's syntax
|
28
|
+
#
|
29
|
+
# @param syntax [String]
|
30
|
+
#
|
31
|
+
def syntax(syntax)
|
32
|
+
self.syntax = syntax
|
33
|
+
end
|
34
|
+
|
35
|
+
#
|
36
|
+
# The command's description, long form
|
37
|
+
#
|
38
|
+
# @param description [String]
|
39
|
+
#
|
40
|
+
def description(description)
|
41
|
+
self.description = description
|
42
|
+
end
|
43
|
+
|
44
|
+
#
|
45
|
+
# The command's summary, short form
|
46
|
+
#
|
47
|
+
# @param summary [String]
|
48
|
+
#
|
49
|
+
def summary(summary)
|
50
|
+
self.summary = summary
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# Defines an example on how to use the command
|
55
|
+
#
|
56
|
+
# @param command [String] The example
|
57
|
+
# @param description [String] Description of the example
|
58
|
+
#
|
59
|
+
def example(command, description)
|
60
|
+
@examples ||= []
|
61
|
+
|
62
|
+
# Commander wants it backwards
|
63
|
+
@examples << [description, command]
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# Defines a command flag (-f, --force)
|
68
|
+
#
|
69
|
+
def option(*args, &block)
|
70
|
+
@options ||= []
|
71
|
+
|
72
|
+
@options << [args, block]
|
73
|
+
end
|
74
|
+
|
75
|
+
#
|
76
|
+
# Defines any aliases for this command
|
77
|
+
#
|
78
|
+
# @param *aliases [Array<String>]
|
79
|
+
#
|
80
|
+
def aliases(*aliases)
|
81
|
+
@aliases ||= []
|
82
|
+
|
83
|
+
@aliases += aliases
|
84
|
+
end
|
85
|
+
|
86
|
+
#
|
87
|
+
# Registers the command with Commander
|
88
|
+
#
|
89
|
+
# @param context [Commander::Command]
|
90
|
+
#
|
91
|
+
# @private
|
92
|
+
#
|
93
|
+
def register(context)
|
94
|
+
raise "Missing command name" if @command_name.nil?
|
95
|
+
|
96
|
+
context.command(@command_name) do |c|
|
97
|
+
c.syntax = @syntax
|
98
|
+
c.summary = @summary
|
99
|
+
c.description = @description
|
100
|
+
c.examples = @examples if @examples
|
101
|
+
|
102
|
+
@options&.each do |opts, block|
|
103
|
+
c.option(*opts, &block)
|
104
|
+
end
|
105
|
+
|
106
|
+
c.action { |args, opts| new(args, opts).call }
|
107
|
+
end
|
108
|
+
|
109
|
+
@aliases&.each do |alii|
|
110
|
+
context.alias_command(alii, @command_name)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
attr_reader :arguments, :options
|
116
|
+
|
117
|
+
#
|
118
|
+
# @param arguments [Array] Any positional arguments
|
119
|
+
# @param options [Hash] Any flag arguments
|
120
|
+
#
|
121
|
+
def initialize(arguments, options)
|
122
|
+
@arguments = arguments
|
123
|
+
@options = options
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class CLI
|
5
|
+
class Init < Command
|
6
|
+
command_name "init"
|
7
|
+
syntax "init"
|
8
|
+
summary "Initializes directory structure and configuration files"
|
9
|
+
|
10
|
+
def call
|
11
|
+
base_path = SpecForge.forge
|
12
|
+
actions.empty_directory "#{base_path}/factories"
|
13
|
+
actions.empty_directory "#{base_path}/specs"
|
14
|
+
|
15
|
+
actions.template(
|
16
|
+
"config.tt",
|
17
|
+
SpecForge.root.join(base_path, "config.yml"),
|
18
|
+
context: binding
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
def default_authorization_value
|
23
|
+
<<~STRING.chomp
|
24
|
+
Bearer <%= ENV.fetch("API_TOKEN", "") %>
|
25
|
+
STRING
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class CLI
|
5
|
+
class New < Command
|
6
|
+
command_name "new"
|
7
|
+
summary "Create a new spec or factory"
|
8
|
+
|
9
|
+
syntax "new <type> <name>"
|
10
|
+
|
11
|
+
example "new spec users",
|
12
|
+
"Creates a new spec located at 'spec_forge/specs/users.yml'"
|
13
|
+
|
14
|
+
example "new factory user",
|
15
|
+
"Creates a new factory located at 'spec_forge/factories/user.yml'"
|
16
|
+
|
17
|
+
example "generate spec accounts",
|
18
|
+
"Uses the generate alias (shorthand 'g') instead of 'new'"
|
19
|
+
|
20
|
+
aliases :generate, :g
|
21
|
+
|
22
|
+
def call
|
23
|
+
type = arguments.first.downcase
|
24
|
+
name = arguments.second
|
25
|
+
|
26
|
+
# Cleanup
|
27
|
+
name.delete_suffix!(".yml") if name.end_with?(".yml")
|
28
|
+
name.delete_suffix!(".yaml") if name.end_with?(".yaml")
|
29
|
+
|
30
|
+
case type
|
31
|
+
when "spec"
|
32
|
+
create_new_spec(name)
|
33
|
+
when "factory"
|
34
|
+
create_new_factory(name)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def create_new_spec(name)
|
41
|
+
actions.create_file(
|
42
|
+
SpecForge.forge.join("specs", "#{name}.yml"),
|
43
|
+
generate_spec(name)
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_new_factory(name)
|
48
|
+
actions.create_file(
|
49
|
+
SpecForge.forge.join("factories", "#{name}.yml"),
|
50
|
+
generate_factory(name)
|
51
|
+
)
|
52
|
+
end
|
53
|
+
|
54
|
+
def generate_spec(name)
|
55
|
+
plural_name = name.pluralize
|
56
|
+
singular_name = name.singularize
|
57
|
+
|
58
|
+
base_spec = {url: ""}
|
59
|
+
base_constraint = {expect: {status: 200}}
|
60
|
+
|
61
|
+
hash = {
|
62
|
+
##################################################
|
63
|
+
"index_#{plural_name}" => base_spec.merge(
|
64
|
+
url: "/#{plural_name}",
|
65
|
+
expectations: [base_constraint]
|
66
|
+
),
|
67
|
+
##################################################
|
68
|
+
"show_#{singular_name}" => base_spec.merge(
|
69
|
+
url: "/#{plural_name}/{id}",
|
70
|
+
expectations: [
|
71
|
+
base_constraint.merge(expect: {status: 404}),
|
72
|
+
base_constraint.deep_merge(
|
73
|
+
query: {id: 1},
|
74
|
+
expect: {
|
75
|
+
json: {
|
76
|
+
name: "kind_of.string",
|
77
|
+
email: /\w+@example\.com/i
|
78
|
+
}
|
79
|
+
}
|
80
|
+
)
|
81
|
+
]
|
82
|
+
),
|
83
|
+
##################################################
|
84
|
+
"create_#{singular_name}" => base_spec.merge(
|
85
|
+
url: "/#{plural_name}",
|
86
|
+
method: "post",
|
87
|
+
expectations: [
|
88
|
+
base_constraint.merge(expect: {status: 400}),
|
89
|
+
base_constraint.deep_merge(
|
90
|
+
variables: {
|
91
|
+
name: "faker.name.name",
|
92
|
+
role: "user"
|
93
|
+
},
|
94
|
+
body: {name: "variables.name"},
|
95
|
+
expect: {
|
96
|
+
json: {name: "variables.name", role: "variables.role"}
|
97
|
+
}
|
98
|
+
)
|
99
|
+
]
|
100
|
+
),
|
101
|
+
##################################################
|
102
|
+
"update_#{singular_name}" => base_spec.merge(
|
103
|
+
url: "/#{plural_name}/{id}",
|
104
|
+
method: "patch",
|
105
|
+
query: {id: 1},
|
106
|
+
variables: {
|
107
|
+
number: {
|
108
|
+
"faker.number.between" => {from: 100_000, to: 999_999}
|
109
|
+
}
|
110
|
+
},
|
111
|
+
expectations: [
|
112
|
+
base_constraint.deep_merge(
|
113
|
+
body: {number: "variables.number"},
|
114
|
+
expect: {
|
115
|
+
json: {name: "kind_of.string", number: "kind_of.integer"}
|
116
|
+
}
|
117
|
+
)
|
118
|
+
]
|
119
|
+
),
|
120
|
+
##################################################
|
121
|
+
"destroy_#{singular_name}" => base_spec.merge(
|
122
|
+
url: "/#{plural_name}/{id}",
|
123
|
+
method: "delete",
|
124
|
+
query: {id: 1},
|
125
|
+
expectations: [
|
126
|
+
base_constraint
|
127
|
+
]
|
128
|
+
)
|
129
|
+
}
|
130
|
+
|
131
|
+
generate_yaml(hash)
|
132
|
+
end
|
133
|
+
|
134
|
+
def generate_factory(name)
|
135
|
+
singular_name = name.singularize
|
136
|
+
|
137
|
+
hash = {
|
138
|
+
singular_name => {
|
139
|
+
class: singular_name.titleize,
|
140
|
+
attributes: {
|
141
|
+
attribute: "value"
|
142
|
+
}
|
143
|
+
}
|
144
|
+
}
|
145
|
+
|
146
|
+
generate_yaml(hash)
|
147
|
+
end
|
148
|
+
|
149
|
+
def generate_yaml(hash)
|
150
|
+
result = hash.deep_stringify_keys.join_map("\n") do |key, value|
|
151
|
+
{key => value}.to_yaml
|
152
|
+
.sub!("---\n", "")
|
153
|
+
.gsub("!ruby/regexp ", "")
|
154
|
+
end
|
155
|
+
|
156
|
+
result.delete!("\"")
|
157
|
+
result
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
class CLI
|
5
|
+
class Run < Command
|
6
|
+
command_name "run"
|
7
|
+
syntax "run"
|
8
|
+
summary "Runs all specs"
|
9
|
+
|
10
|
+
# option "-n", "--no-docs", "Do not generate OpenAPI documentation on completion"
|
11
|
+
|
12
|
+
def call
|
13
|
+
SpecForge.run
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "cli/actions"
|
4
|
+
require_relative "cli/command"
|
5
|
+
require_relative "cli/init"
|
6
|
+
require_relative "cli/new"
|
7
|
+
require_relative "cli/run"
|
8
|
+
|
9
|
+
module SpecForge
|
10
|
+
class CLI
|
11
|
+
include Commander::Methods
|
12
|
+
|
13
|
+
COMMANDS = [Init, New, Run]
|
14
|
+
|
15
|
+
#
|
16
|
+
# Runs the CLI
|
17
|
+
#
|
18
|
+
# @private
|
19
|
+
#
|
20
|
+
def run
|
21
|
+
program :name, "SpecForge"
|
22
|
+
program :version, SpecForge::VERSION
|
23
|
+
program :description, "Write expressive API tests in YAML with the power of RSpec matchers"
|
24
|
+
|
25
|
+
register_commands
|
26
|
+
|
27
|
+
default_command :run
|
28
|
+
|
29
|
+
run!
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# Registers the commands with Commander
|
34
|
+
#
|
35
|
+
# @private
|
36
|
+
#
|
37
|
+
def register_commands
|
38
|
+
COMMANDS.each do |command_class|
|
39
|
+
command_class.register(self)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module SpecForge
|
4
|
+
#
|
5
|
+
# Represents `config.yml`
|
6
|
+
#
|
7
|
+
class Config < Struct.new(:base_url, :authorization, :factories, :environment)
|
8
|
+
#
|
9
|
+
# authorization: {}
|
10
|
+
#
|
11
|
+
class Authorization < Struct.new(:header, :value)
|
12
|
+
attr_predicate :header, :value
|
13
|
+
end
|
14
|
+
|
15
|
+
#
|
16
|
+
# factories: {}
|
17
|
+
#
|
18
|
+
class Factories < Struct.new(:paths, :auto_discover)
|
19
|
+
attr_predicate :paths, :auto_discover
|
20
|
+
end
|
21
|
+
|
22
|
+
#
|
23
|
+
# environment: {}
|
24
|
+
#
|
25
|
+
class Environment < Struct.new(:use, :preload, :models_path)
|
26
|
+
attr_predicate :use, :preload, :models_path
|
27
|
+
|
28
|
+
def initialize(string_or_hash)
|
29
|
+
use, preload, models_path = "", "", ""
|
30
|
+
|
31
|
+
# "rails" or other preset
|
32
|
+
if string_or_hash.is_a?(String)
|
33
|
+
use = string_or_hash
|
34
|
+
else
|
35
|
+
string_or_hash => {use:, preload:, models_path:}
|
36
|
+
end
|
37
|
+
|
38
|
+
super(use:, preload:, models_path:)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
############################################################################
|
43
|
+
|
44
|
+
#
|
45
|
+
# Creates a config with the user's config overlaid on the default
|
46
|
+
#
|
47
|
+
def initialize
|
48
|
+
config = Normalizer.default_config.deep_merge(load_from_file)
|
49
|
+
normalized = Normalizer.normalize_config!(config)
|
50
|
+
|
51
|
+
super(
|
52
|
+
base_url: normalized.delete(:base_url),
|
53
|
+
authorization: transform_authorization(normalized),
|
54
|
+
factories: transform_factories(normalized),
|
55
|
+
environment: transform_environment(normalized)
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def load_from_file
|
62
|
+
path = SpecForge.forge.join("config.yml")
|
63
|
+
return unless File.exist?(path)
|
64
|
+
|
65
|
+
erb = ERB.new(File.read(path)).result
|
66
|
+
YAML.safe_load(erb, aliases: true, symbolize_names: true)
|
67
|
+
end
|
68
|
+
|
69
|
+
def transform_authorization(hash)
|
70
|
+
# The intention for authorization hash is to support different authorization schemes
|
71
|
+
# authorization: {default: {}, admin: {}, dev: {}}
|
72
|
+
# But I won't know exactly what will be defined - `to_struct` will handle that.
|
73
|
+
hash[:authorization].transform_values { |v| Authorization.new(**v) }.to_struct
|
74
|
+
end
|
75
|
+
|
76
|
+
def transform_factories(hash)
|
77
|
+
Factories.new(**hash[:factories])
|
78
|
+
end
|
79
|
+
|
80
|
+
def transform_environment(hash)
|
81
|
+
Environment.new(hash[:environment])
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|