beachio-hammer 0.1.0.pre6 → 0.1.0.pre7
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/PUBLISH.md +5 -1
- data/lib/hammer/check_command.rb +210 -0
- data/lib/hammer/version.rb +1 -1
- data/spec/check_command_spec.rb +276 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7f49550d59c10cbcb9b8ed9dae068082d5962abd833fcf2b6b039d07ce409b5e
|
|
4
|
+
data.tar.gz: 4e04f20a894021945d2366a00a4b4239404541d139d60ed99c93c49afff767e5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 69c67662fc6d413a3af72e8873ad96345e8e363a87fac6657c1c10385239ab195201805c2e9d1784beae717316f15e9835ba81f1483e39f43b8028c971d03ab4
|
|
7
|
+
data.tar.gz: 0efecd32fde11e6c6b6be017f427554cd7d57c11ad633d766c8b61be499b660cf7f61928e25dbcfd53db52cfe1ed765016c0ab0a5aa0d71a13503b921ed4f029
|
data/PUBLISH.md
CHANGED
|
@@ -38,7 +38,11 @@ gem build beachio-hammer.gemspec
|
|
|
38
38
|
gem push beachio-hammer-*.gem
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
Prerelease pin for executor team: **`beachio-hammer` `0.1.0.
|
|
41
|
+
Prerelease pin for executor team: **`beachio-hammer` `0.1.0.pre7`**
|
|
42
|
+
|
|
43
|
+
### 0.1.0.pre7 changes
|
|
44
|
+
|
|
45
|
+
- Fix gem packaging: include `lib/hammer/check_command.rb` (pre6 omitted untracked file)
|
|
42
46
|
|
|
43
47
|
### 0.1.0.pre6 changes (`hammer check`)
|
|
44
48
|
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hammer
|
|
4
|
+
module CheckCommand
|
|
5
|
+
module_function
|
|
6
|
+
|
|
7
|
+
def run(argv)
|
|
8
|
+
options = parse_options(argv)
|
|
9
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
10
|
+
|
|
11
|
+
project_dir, project_exit = resolve_project(options)
|
|
12
|
+
return project_exit if project_exit
|
|
13
|
+
|
|
14
|
+
errors = []
|
|
15
|
+
warnings = []
|
|
16
|
+
files_checked = 0
|
|
17
|
+
|
|
18
|
+
config_path = File.join(project_dir, "content.config.json")
|
|
19
|
+
|
|
20
|
+
unless File.file?(config_path)
|
|
21
|
+
warnings << issue(
|
|
22
|
+
rule_id: "CONFIG_MISSING",
|
|
23
|
+
message: "No content.config.json found",
|
|
24
|
+
file: project_dir,
|
|
25
|
+
suggestion: "Create a content.config.json to enable content validation"
|
|
26
|
+
)
|
|
27
|
+
duration_ms = elapsed_ms(start)
|
|
28
|
+
status = options[:strict] && !warnings.empty? ? "fail" : "pass"
|
|
29
|
+
output_result(status, errors, warnings, files_checked, duration_ms, options[:format])
|
|
30
|
+
return options[:strict] && !warnings.empty? ? 1 : 0
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
config = load_config(config_path, errors, warnings, files_checked, start, options[:format])
|
|
34
|
+
return 1 unless config
|
|
35
|
+
|
|
36
|
+
parser = Content::Parser.new(source_directory: project_dir)
|
|
37
|
+
|
|
38
|
+
config.collections.each do |collection_name, collection_config|
|
|
39
|
+
content_dir = File.join(project_dir, collection_config.path)
|
|
40
|
+
|
|
41
|
+
unless File.directory?(content_dir)
|
|
42
|
+
errors << issue(
|
|
43
|
+
rule_id: "COLLECTION_DIR_MISSING",
|
|
44
|
+
message: "Content directory not found for collection '#{collection_name}'",
|
|
45
|
+
file: collection_config.path,
|
|
46
|
+
suggestion: "Create the directory at #{collection_config.path}"
|
|
47
|
+
)
|
|
48
|
+
next
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
if collection_config.template
|
|
52
|
+
template_path = File.join(project_dir, collection_config.template)
|
|
53
|
+
unless File.file?(template_path)
|
|
54
|
+
errors << issue(
|
|
55
|
+
rule_id: "TEMPLATE_MISSING",
|
|
56
|
+
message: "Template not found for collection '#{collection_name}'",
|
|
57
|
+
file: collection_config.template,
|
|
58
|
+
suggestion: "Create the template file at #{collection_config.template}"
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
file_extension = collection_config.format || "md"
|
|
64
|
+
discover_content_files(content_dir, file_extension).each do |file_path|
|
|
65
|
+
files_checked += 1
|
|
66
|
+
relative_path = file_path.delete_prefix("#{project_dir}/")
|
|
67
|
+
|
|
68
|
+
document = parse_content_file(parser, file_path, file_extension, relative_path, errors)
|
|
69
|
+
next unless document
|
|
70
|
+
|
|
71
|
+
collection_config.validate_required_fields(document.metadata).each do |field|
|
|
72
|
+
errors << issue(
|
|
73
|
+
rule_id: "REQUIRED_FIELD_MISSING",
|
|
74
|
+
message: "Missing required field '#{field}' in collection '#{collection_name}'",
|
|
75
|
+
file: relative_path,
|
|
76
|
+
suggestion: "Add '#{field}' to the front matter"
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
next unless collection_config.slug
|
|
81
|
+
|
|
82
|
+
slug_config = collection_config.slug
|
|
83
|
+
slug_field = slug_config["field"] || slug_config[:field]
|
|
84
|
+
slug_fallback = slug_config["fallback"] || slug_config[:fallback]
|
|
85
|
+
slug_value = document.metadata[slug_field] || document.metadata[slug_fallback]
|
|
86
|
+
next unless slug_value.nil? || (slug_value.is_a?(String) && slug_value.empty?)
|
|
87
|
+
|
|
88
|
+
warnings << issue(
|
|
89
|
+
rule_id: "SLUG_MISSING",
|
|
90
|
+
message: "No slug or fallback value for collection '#{collection_name}'",
|
|
91
|
+
file: relative_path,
|
|
92
|
+
suggestion: "Add '#{slug_field}' or '#{slug_fallback}' to front matter"
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
duration_ms = elapsed_ms(start)
|
|
98
|
+
has_failed = !errors.empty? || (options[:strict] && !warnings.empty?)
|
|
99
|
+
status = has_failed ? "fail" : "pass"
|
|
100
|
+
output_result(status, errors, warnings, files_checked, duration_ms, options[:format])
|
|
101
|
+
has_failed ? 1 : 0
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def parse_options(argv)
|
|
105
|
+
options = {
|
|
106
|
+
project: nil,
|
|
107
|
+
strict: false,
|
|
108
|
+
format: :human
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
i = 0
|
|
112
|
+
while i < argv.length
|
|
113
|
+
arg = argv[i]
|
|
114
|
+
case arg
|
|
115
|
+
when "--project"
|
|
116
|
+
options[:project] = argv[i += 1]
|
|
117
|
+
when "--strict"
|
|
118
|
+
options[:strict] = true
|
|
119
|
+
when "--format"
|
|
120
|
+
fmt = argv[i += 1].to_s.downcase
|
|
121
|
+
options[:format] = fmt == "json" ? :json : :human
|
|
122
|
+
else
|
|
123
|
+
warn "Unknown option: #{arg}"
|
|
124
|
+
raise ConfigError, "Unknown option: #{arg}"
|
|
125
|
+
end
|
|
126
|
+
i += 1
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
options
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def resolve_project(options)
|
|
133
|
+
[ProjectResolver.resolve(explicit_path: options[:project]), nil]
|
|
134
|
+
rescue ConfigError => e
|
|
135
|
+
if options[:format] == :json
|
|
136
|
+
OutputFormatter.format_check_result(
|
|
137
|
+
status: "error",
|
|
138
|
+
errors: [issue(
|
|
139
|
+
rule_id: "PROJECT_NOT_FOUND",
|
|
140
|
+
message: e.message,
|
|
141
|
+
file: "",
|
|
142
|
+
suggestion: "Specify a project path with --project"
|
|
143
|
+
)],
|
|
144
|
+
warnings: [],
|
|
145
|
+
files_checked: 0,
|
|
146
|
+
duration_ms: 0,
|
|
147
|
+
format: :json
|
|
148
|
+
)
|
|
149
|
+
else
|
|
150
|
+
warn "Error: #{e.message}"
|
|
151
|
+
end
|
|
152
|
+
[nil, 2]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def load_config(config_path, errors, warnings, files_checked, start, format)
|
|
156
|
+
Content::Config.load(config_path)
|
|
157
|
+
rescue StandardError => e
|
|
158
|
+
errors << issue(
|
|
159
|
+
rule_id: "CONFIG_INVALID",
|
|
160
|
+
message: "content.config.json is invalid: #{e.message}",
|
|
161
|
+
file: "content.config.json",
|
|
162
|
+
suggestion: "Check the JSON syntax and structure"
|
|
163
|
+
)
|
|
164
|
+
duration_ms = elapsed_ms(start)
|
|
165
|
+
output_result("fail", errors, warnings, files_checked, duration_ms, format)
|
|
166
|
+
nil
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def discover_content_files(content_dir, file_extension)
|
|
170
|
+
Dir.glob(File.join(content_dir, "**", "*")).select do |path|
|
|
171
|
+
File.file?(path) && File.extname(path).delete_prefix(".") == file_extension
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def parse_content_file(parser, file_path, file_extension, relative_path, errors)
|
|
176
|
+
if file_extension == "md"
|
|
177
|
+
parser.parse_markdown_file(file_path)
|
|
178
|
+
else
|
|
179
|
+
parser.parse_yaml_file(file_path)
|
|
180
|
+
end
|
|
181
|
+
rescue StandardError => e
|
|
182
|
+
errors << issue(
|
|
183
|
+
rule_id: "PARSE_ERROR",
|
|
184
|
+
message: "Failed to parse content file: #{e.message}",
|
|
185
|
+
file: relative_path,
|
|
186
|
+
suggestion: "Check the file format and front matter syntax"
|
|
187
|
+
)
|
|
188
|
+
nil
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def issue(rule_id:, message:, file:, suggestion:)
|
|
192
|
+
{ rule_id: rule_id, message: message, file: file, suggestion: suggestion }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def output_result(status, errors, warnings, files_checked, duration_ms, format)
|
|
196
|
+
OutputFormatter.format_check_result(
|
|
197
|
+
status: status,
|
|
198
|
+
errors: errors,
|
|
199
|
+
warnings: warnings,
|
|
200
|
+
files_checked: files_checked,
|
|
201
|
+
duration_ms: duration_ms,
|
|
202
|
+
format: format
|
|
203
|
+
)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def elapsed_ms(start)
|
|
207
|
+
((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).to_i
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
data/lib/hammer/version.rb
CHANGED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "json"
|
|
6
|
+
require "stringio"
|
|
7
|
+
require "tmpdir"
|
|
8
|
+
|
|
9
|
+
CONTENT_FIXTURE = File.expand_path("../../Test Projects/content-generation-test", __dir__)
|
|
10
|
+
|
|
11
|
+
RSpec.describe Hammer::CheckCommand do
|
|
12
|
+
|
|
13
|
+
def capture_check(argv, project: CONTENT_FIXTURE)
|
|
14
|
+
out = StringIO.new
|
|
15
|
+
original = $stdout
|
|
16
|
+
$stdout = out
|
|
17
|
+
exit_code = Hammer::CheckCommand.run(["--project", project, *argv])
|
|
18
|
+
[out.string, exit_code]
|
|
19
|
+
ensure
|
|
20
|
+
$stdout = original
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe "content-generation-test fixture" do
|
|
24
|
+
it "passes validation" do
|
|
25
|
+
stdout, exit_code = capture_check(["--format", "json"])
|
|
26
|
+
result = JSON.parse(stdout)
|
|
27
|
+
|
|
28
|
+
expect(exit_code).to eq(0)
|
|
29
|
+
expect(result["status"]).to eq("pass")
|
|
30
|
+
expect(result["errors"]).to be_empty
|
|
31
|
+
expect(result["stats"]["filesChecked"]).to be > 0
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "passes with --strict when no warnings" do
|
|
35
|
+
_stdout, exit_code = capture_check(["--strict", "--format", "json"])
|
|
36
|
+
expect(exit_code).to eq(0)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe "CONFIG_MISSING" do
|
|
41
|
+
it "passes with warning when not strict" do
|
|
42
|
+
Dir.mktmpdir do |dir|
|
|
43
|
+
stdout, exit_code = capture_check(["--format", "json"], project: dir)
|
|
44
|
+
result = JSON.parse(stdout)
|
|
45
|
+
|
|
46
|
+
expect(exit_code).to eq(0)
|
|
47
|
+
expect(result["status"]).to eq("pass")
|
|
48
|
+
expect(result["warnings"].map { |w| w["ruleId"] }).to include("CONFIG_MISSING")
|
|
49
|
+
expect(result["stats"]["filesChecked"]).to eq(0)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "fails with --strict" do
|
|
54
|
+
Dir.mktmpdir do |dir|
|
|
55
|
+
stdout, exit_code = capture_check(["--strict", "--format", "json"], project: dir)
|
|
56
|
+
result = JSON.parse(stdout)
|
|
57
|
+
|
|
58
|
+
expect(exit_code).to eq(1)
|
|
59
|
+
expect(result["status"]).to eq("fail")
|
|
60
|
+
expect(result["warnings"].map { |w| w["ruleId"] }).to include("CONFIG_MISSING")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
describe "CONFIG_INVALID" do
|
|
66
|
+
it "fails and does not scan files" do
|
|
67
|
+
Dir.mktmpdir do |dir|
|
|
68
|
+
File.write(File.join(dir, "content.config.json"), "{ invalid json")
|
|
69
|
+
stdout, exit_code = capture_check(["--format", "json"], project: dir)
|
|
70
|
+
result = JSON.parse(stdout)
|
|
71
|
+
|
|
72
|
+
expect(exit_code).to eq(1)
|
|
73
|
+
expect(result["status"]).to eq("fail")
|
|
74
|
+
expect(result["errors"].map { |e| e["ruleId"] }).to include("CONFIG_INVALID")
|
|
75
|
+
expect(result["stats"]["filesChecked"]).to eq(0)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
describe "COLLECTION_DIR_MISSING" do
|
|
81
|
+
it "reports missing collection directory" do
|
|
82
|
+
Dir.mktmpdir do |dir|
|
|
83
|
+
File.write(
|
|
84
|
+
File.join(dir, "content.config.json"),
|
|
85
|
+
JSON.generate(
|
|
86
|
+
"collections" => {
|
|
87
|
+
"posts" => { "path" => "content/missing" }
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
stdout, exit_code = capture_check(["--format", "json"], project: dir)
|
|
92
|
+
result = JSON.parse(stdout)
|
|
93
|
+
|
|
94
|
+
expect(exit_code).to eq(1)
|
|
95
|
+
expect(result["errors"].map { |e| e["ruleId"] }).to include("COLLECTION_DIR_MISSING")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe "TEMPLATE_MISSING" do
|
|
101
|
+
it "reports missing template file" do
|
|
102
|
+
Dir.mktmpdir do |dir|
|
|
103
|
+
FileUtils.mkdir_p(File.join(dir, "content", "posts"))
|
|
104
|
+
File.write(
|
|
105
|
+
File.join(dir, "content.config.json"),
|
|
106
|
+
JSON.generate(
|
|
107
|
+
"collections" => {
|
|
108
|
+
"posts" => {
|
|
109
|
+
"path" => "content/posts",
|
|
110
|
+
"template" => "templates/missing.html"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
stdout, exit_code = capture_check(["--format", "json"], project: dir)
|
|
116
|
+
result = JSON.parse(stdout)
|
|
117
|
+
|
|
118
|
+
expect(exit_code).to eq(1)
|
|
119
|
+
expect(result["errors"].map { |e| e["ruleId"] }).to include("TEMPLATE_MISSING")
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
describe "REQUIRED_FIELD_MISSING" do
|
|
125
|
+
it "reports missing required front matter fields" do
|
|
126
|
+
Dir.mktmpdir do |dir|
|
|
127
|
+
FileUtils.mkdir_p(File.join(dir, "content", "posts"))
|
|
128
|
+
File.write(
|
|
129
|
+
File.join(dir, "content.config.json"),
|
|
130
|
+
JSON.generate(
|
|
131
|
+
"collections" => {
|
|
132
|
+
"posts" => {
|
|
133
|
+
"path" => "content/posts",
|
|
134
|
+
"fields" => {
|
|
135
|
+
"title" => { "type" => "string", "required" => true }
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
File.write(File.join(dir, "content", "posts", "no-title.md"), "---\ndate: 2024-01-01\n---\n\nBody")
|
|
142
|
+
stdout, exit_code = capture_check(["--format", "json"], project: dir)
|
|
143
|
+
result = JSON.parse(stdout)
|
|
144
|
+
|
|
145
|
+
expect(exit_code).to eq(1)
|
|
146
|
+
expect(result["errors"].map { |e| e["ruleId"] }).to include("REQUIRED_FIELD_MISSING")
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
describe "SLUG_MISSING" do
|
|
152
|
+
it "warns when slug and fallback are absent" do
|
|
153
|
+
Dir.mktmpdir do |dir|
|
|
154
|
+
FileUtils.mkdir_p(File.join(dir, "content", "posts"))
|
|
155
|
+
File.write(
|
|
156
|
+
File.join(dir, "content.config.json"),
|
|
157
|
+
JSON.generate(
|
|
158
|
+
"collections" => {
|
|
159
|
+
"posts" => {
|
|
160
|
+
"path" => "content/posts",
|
|
161
|
+
"slug" => { "field" => "slug", "fallback" => "title" }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
File.write(File.join(dir, "content", "posts", "post.md"), "---\ndate: 2024-01-01\n---\n\nBody")
|
|
167
|
+
stdout, exit_code = capture_check(["--format", "json"], project: dir)
|
|
168
|
+
result = JSON.parse(stdout)
|
|
169
|
+
|
|
170
|
+
expect(exit_code).to eq(0)
|
|
171
|
+
expect(result["warnings"].map { |w| w["ruleId"] }).to include("SLUG_MISSING")
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it "fails with --strict" do
|
|
176
|
+
Dir.mktmpdir do |dir|
|
|
177
|
+
FileUtils.mkdir_p(File.join(dir, "content", "posts"))
|
|
178
|
+
File.write(
|
|
179
|
+
File.join(dir, "content.config.json"),
|
|
180
|
+
JSON.generate(
|
|
181
|
+
"collections" => {
|
|
182
|
+
"posts" => {
|
|
183
|
+
"path" => "content/posts",
|
|
184
|
+
"slug" => { "field" => "slug", "fallback" => "title" }
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
File.write(File.join(dir, "content", "posts", "post.md"), "---\ndate: 2024-01-01\n---\n\nBody")
|
|
190
|
+
_stdout, exit_code = capture_check(["--strict", "--format", "json"], project: dir)
|
|
191
|
+
expect(exit_code).to eq(1)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
describe "PARSE_ERROR" do
|
|
197
|
+
it "reports unparseable content files" do
|
|
198
|
+
Dir.mktmpdir do |dir|
|
|
199
|
+
FileUtils.mkdir_p(File.join(dir, "content", "posts"))
|
|
200
|
+
File.write(
|
|
201
|
+
File.join(dir, "content.config.json"),
|
|
202
|
+
JSON.generate("collections" => { "posts" => { "path" => "content/posts" } })
|
|
203
|
+
)
|
|
204
|
+
allow(Hammer::Content::Parser).to receive(:new).and_wrap_original do |method, **kwargs|
|
|
205
|
+
parser = method.call(**kwargs)
|
|
206
|
+
allow(parser).to receive(:parse_markdown_file).and_raise(StandardError, "bad front matter")
|
|
207
|
+
parser
|
|
208
|
+
end
|
|
209
|
+
File.write(File.join(dir, "content", "posts", "bad.md"), "---\ntitle: ok\n---\n\nBody")
|
|
210
|
+
stdout, exit_code = capture_check(["--format", "json"], project: dir)
|
|
211
|
+
result = JSON.parse(stdout)
|
|
212
|
+
|
|
213
|
+
expect(exit_code).to eq(1)
|
|
214
|
+
expect(result["errors"].map { |e| e["ruleId"] }).to include("PARSE_ERROR")
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
describe "PROJECT_NOT_FOUND" do
|
|
220
|
+
it "returns exit 2 with JSON error status" do
|
|
221
|
+
stdout, exit_code = capture_check(["--format", "json"], project: "/nonexistent/path")
|
|
222
|
+
result = JSON.parse(stdout)
|
|
223
|
+
|
|
224
|
+
expect(exit_code).to eq(2)
|
|
225
|
+
expect(result["status"]).to eq("error")
|
|
226
|
+
expect(result["errors"].first["ruleId"]).to eq("PROJECT_NOT_FOUND")
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
describe "CollectionConfig#validate_required_fields" do
|
|
231
|
+
it "flags missing keys and empty strings" do
|
|
232
|
+
config = Hammer::Content::CollectionConfig.new(
|
|
233
|
+
fields: {
|
|
234
|
+
"title" => Hammer::Content::FieldConfig.new(required: true),
|
|
235
|
+
"count" => Hammer::Content::FieldConfig.new(required: true)
|
|
236
|
+
}
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
missing = config.validate_required_fields("title" => "", "count" => 0)
|
|
240
|
+
expect(missing).to contain_exactly("title")
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
RSpec.describe Hammer::CLI do
|
|
246
|
+
def capture_cli(argv)
|
|
247
|
+
out = StringIO.new
|
|
248
|
+
original = $stdout
|
|
249
|
+
$stdout = out
|
|
250
|
+
exit_code = Hammer::CLI.run(argv)
|
|
251
|
+
[out.string, exit_code]
|
|
252
|
+
ensure
|
|
253
|
+
$stdout = original
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
it "dispatches hammer check" do
|
|
257
|
+
stdout, exit_code = capture_cli(["check", "--project", CONTENT_FIXTURE, "--format", "json"])
|
|
258
|
+
result = JSON.parse(stdout)
|
|
259
|
+
|
|
260
|
+
expect(exit_code).to eq(0)
|
|
261
|
+
expect(result["status"]).to eq("pass")
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
it "defaults to build for flag-only invocation" do
|
|
265
|
+
stdout, exit_code = capture_cli(["--project", CONTENT_FIXTURE, "--format", "json", "--quiet"])
|
|
266
|
+
result = JSON.parse(stdout)
|
|
267
|
+
|
|
268
|
+
expect(exit_code).to eq(0)
|
|
269
|
+
expect(result["status"]).to eq("success")
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
it "returns exit 2 for unknown commands" do
|
|
273
|
+
_stdout, exit_code = capture_cli(["unknown"])
|
|
274
|
+
expect(exit_code).to eq(2)
|
|
275
|
+
end
|
|
276
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: beachio-hammer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.0.
|
|
4
|
+
version: 0.1.0.pre7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Beach.io
|
|
@@ -54,6 +54,7 @@ files:
|
|
|
54
54
|
- lib/beachio-hammer.rb
|
|
55
55
|
- lib/hammer/build_command.rb
|
|
56
56
|
- lib/hammer/builder.rb
|
|
57
|
+
- lib/hammer/check_command.rb
|
|
57
58
|
- lib/hammer/cli.rb
|
|
58
59
|
- lib/hammer/content/builder.rb
|
|
59
60
|
- lib/hammer/content/config.rb
|
|
@@ -78,6 +79,7 @@ files:
|
|
|
78
79
|
- scripts/test-content.rb
|
|
79
80
|
- scripts/tree-diff.rb
|
|
80
81
|
- spec/blog_listing_spec.rb
|
|
82
|
+
- spec/check_command_spec.rb
|
|
81
83
|
- spec/encoding_spec.rb
|
|
82
84
|
- spec/parity/classic_spec.rb
|
|
83
85
|
- spec/parity/content_spec.rb
|