beachio-hammer 0.1.0.pre6 → 0.1.0.pre8
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/tag_parser.rb +10 -2
- data/lib/hammer/version.rb +1 -1
- data/spec/check_command_spec.rb +276 -0
- data/spec/path_resolution_spec.rb +9 -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: 589ceae0ff8729f2cb3ecc0ee32d4838ace1138571c9c86d71ca78c17056e528
|
|
4
|
+
data.tar.gz: dca32a180fecf6664e35f2b6731190fc05c52fcf4833df8f0046dcb9030eb5f6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b150e39c1399ca2ae16ca81b6bc605cf5c98ce7a4574d802a575319de6b2bbd422f70a1c4c9b710da351d0d48003960ba31658cb0968f18388f665ff40630e3a
|
|
7
|
+
data.tar.gz: 846a0033ac0b6a5f3da13e50c543eada11d89902c809f8db1970e90c7a03ff90e308e8ba3f752fc2f3ea1b63673ad67e30f4be5203d9d431d20ae091151d9961
|
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/tag_parser.rb
CHANGED
|
@@ -439,8 +439,15 @@ module Hammer
|
|
|
439
439
|
end
|
|
440
440
|
|
|
441
441
|
def resolve_path_reference(ref)
|
|
442
|
+
fragment = ""
|
|
443
|
+
if ref.include?("#")
|
|
444
|
+
base, anchor = ref.split("#", 2)
|
|
445
|
+
ref = base
|
|
446
|
+
fragment = "##{anchor}" unless anchor.nil? || anchor.empty?
|
|
447
|
+
end
|
|
448
|
+
|
|
442
449
|
if ref == "/"
|
|
443
|
-
return root_relative_path
|
|
450
|
+
return "#{root_relative_path}#{fragment}"
|
|
444
451
|
end
|
|
445
452
|
|
|
446
453
|
preserve_slash = ref.end_with?("/")
|
|
@@ -462,7 +469,8 @@ module Hammer
|
|
|
462
469
|
|
|
463
470
|
@delegate&.warning(@current_file_path, "File not found: #{ref}") if paths.empty?
|
|
464
471
|
|
|
465
|
-
generate_context_aware_path(relative_path, preserve_trailing_slash: preserve_slash)
|
|
472
|
+
resolved = generate_context_aware_path(relative_path, preserve_trailing_slash: preserve_slash)
|
|
473
|
+
"#{resolved}#{fragment}"
|
|
466
474
|
end
|
|
467
475
|
|
|
468
476
|
def root_relative_path
|
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
|
|
@@ -24,6 +24,15 @@ RSpec.describe Hammer::TagParser do
|
|
|
24
24
|
expect(result).to include('href="../../"')
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
it "preserves URL fragments when resolving @path to a page" do
|
|
28
|
+
parser.current_file_path = "index.html"
|
|
29
|
+
content = '<a href="<!-- @path work/mixfit/#team -->">'
|
|
30
|
+
|
|
31
|
+
result = parser.send(:parse, content)
|
|
32
|
+
|
|
33
|
+
expect(result).to match(/href="[^"]*mixfit\/#team"/)
|
|
34
|
+
end
|
|
35
|
+
|
|
27
36
|
it "does not corrupt digits in image filenames when resolving @path" do
|
|
28
37
|
parser.current_file_path = "products/forge/index.html"
|
|
29
38
|
content = '<img src="<!-- @path assets/images/products/forge-2.jpg -->" />'
|
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.pre8
|
|
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
|