yaml-janitor 20251113

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 022b412fb7fefdf3b91aae1bb4d47db513c144e1ddb3e99b6d0b83ee9141ac90
4
+ data.tar.gz: 0d0f1fd010e75a0bceda041960f03432b283091fb8425b2909d3f0f4beaadc80
5
+ SHA512:
6
+ metadata.gz: 18655c70a33f9db707541e76d6951bbb98b3f227028f933101ff8dd0ea28d2b0fa319a36af467ccb8b52ea7bb46727ef95d8e6625a5b1d647ebdda6ee4601958
7
+ data.tar.gz: d48f8f842813eb5538c4e939969736b74b1fc8c2414f7be91d9890108d57336015276c6802b35ee138414e88175305c43cf2b85732e9c8ce95873c893083ad1b
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ducks
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,185 @@
1
+ # yaml-janitor
2
+
3
+ A YAML linter built on psych-pure that preserves comments while detecting and
4
+ fixing issues.
5
+
6
+ ## Why?
7
+
8
+ Traditional YAML tools destroy comments when editing files. yaml-janitor uses
9
+ psych-pure's comment-preserving parser to lint and fix YAML files without
10
+ losing valuable documentation.
11
+
12
+ ## Installation
13
+
14
+ ```bash
15
+ gem install yaml-janitor
16
+ ```
17
+
18
+ Or in your Gemfile:
19
+
20
+ ```ruby
21
+ gem 'yaml-janitor'
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ### CLI
27
+
28
+ Check a single file:
29
+ ```bash
30
+ yaml-janitor config.yml
31
+ ```
32
+
33
+ Check all YAML files in a directory:
34
+ ```bash
35
+ yaml-janitor containers/
36
+ ```
37
+
38
+ Auto-fix issues:
39
+ ```bash
40
+ yaml-janitor --fix config.yml
41
+ ```
42
+
43
+ Run specific rules:
44
+ ```bash
45
+ yaml-janitor --rules multiline_certificate config.yml
46
+ ```
47
+
48
+ ### Ruby API
49
+
50
+ ```ruby
51
+ require 'yaml_janitor'
52
+
53
+ # Lint a file
54
+ result = YamlJanitor.lint_file("config.yml")
55
+ result[:violations].each do |violation|
56
+ puts violation
57
+ end
58
+
59
+ # Lint and fix
60
+ result = YamlJanitor.lint_file("config.yml", fix: true)
61
+ if result[:fixed]
62
+ puts "Fixed! New content:\n#{result[:output]}"
63
+ end
64
+
65
+ # Lint a string
66
+ yaml_string = File.read("config.yml")
67
+ result = YamlJanitor.lint(yaml_string)
68
+ ```
69
+
70
+ ## Configuration
71
+
72
+ Create a `.yaml-janitor.yml` file in your project root:
73
+
74
+ ```yaml
75
+ # Formatting options (applied during --fix)
76
+ indentation: 2
77
+ line_width: 80
78
+ sequence_indent: false
79
+
80
+ # Rule configuration
81
+ rules:
82
+ multiline_certificate:
83
+ enabled: true
84
+ consistent_indentation:
85
+ enabled: true
86
+ ```
87
+
88
+ ### Configuration Options
89
+
90
+ **Formatting**:
91
+ - `indentation`: Number of spaces for indentation (default: 2)
92
+ - `line_width`: Maximum line width before wrapping (default: 80)
93
+ - `sequence_indent`: Indent sequences under their key (default: false)
94
+
95
+ **Rules**:
96
+ - `multiline_certificate`: Detects multi-line certificates in double-quoted strings
97
+ - `consistent_indentation`: Detects and fixes inconsistent indentation
98
+
99
+ ### Command Line Overrides
100
+
101
+ ```bash
102
+ # Override config file settings
103
+ yaml-janitor --indentation 4 --line-width 100 config.yml
104
+
105
+ # Use a specific config file
106
+ yaml-janitor --config production.yml containers/
107
+ ```
108
+
109
+ ## Rules
110
+
111
+ ### multiline_certificate
112
+
113
+ Detects multi-line certificates embedded in double-quoted strings. This pattern
114
+ triggers a psych-pure parser bug.
115
+
116
+ ```yaml
117
+ # BAD (will trigger violation)
118
+ DISCOURSE_SAML_CERT: "-----BEGIN CERTIFICATE-----
119
+ MIIDGDCCAgCgAwIBAgIVAMP/9hm9Vl3/23QoXrL8hQ31DLwRMA0GCSqGSIb3DQEB
120
+ -----END CERTIFICATE-----"
121
+
122
+ # GOOD (use block literal style)
123
+ DISCOURSE_SAML_CERT: |
124
+ -----BEGIN CERTIFICATE-----
125
+ MIIDGDCCAgCgAwIBAgIVAMP/9hm9Vl3/23QoXrL8hQ31DLwRMA0GCSqGSIb3DQEB
126
+ -----END CERTIFICATE-----
127
+ ```
128
+
129
+ **Auto-fix**: Not yet implemented (requires psych-pure enhancements)
130
+
131
+ ### consistent_indentation
132
+
133
+ Detects inconsistent indentation (mixing 2-space, 4-space, etc.) in YAML files.
134
+
135
+ ```yaml
136
+ # BAD (inconsistent: 4 and 8 spaces)
137
+ database:
138
+ host: "localhost"
139
+ config:
140
+ timeout: 30
141
+
142
+ # GOOD (consistent: 2 spaces)
143
+ database:
144
+ host: "localhost"
145
+ config:
146
+ timeout: 30
147
+ ```
148
+
149
+ **Auto-fix**: Yes, normalizes to configured indentation (default: 2 spaces)
150
+
151
+ ## Development
152
+
153
+ ### Running Tests
154
+
155
+ ```bash
156
+ # Run integration tests
157
+ ruby -I lib test/integration_test.rb
158
+
159
+ # Or with rake (if configured)
160
+ bundle install
161
+ bundle exec rake test
162
+ ```
163
+
164
+ ### Test Coverage
165
+
166
+ Integration tests verify:
167
+ - Comment preservation during fixes
168
+ - Indentation normalization
169
+ - Paranoid mode (semantic verification)
170
+ - Config loading and rule enable/disable
171
+ - Multi-line certificate detection
172
+ - Clean files pass without violations
173
+
174
+ ## Background
175
+
176
+ This tool was built to support YAML comment preservation in Discourse ops
177
+ automation. See the original discussion:
178
+ https://dev.discourse.org/t/should-we-lint-our-yaml-files/33593
179
+
180
+ Built on top of Kevin Newton's psych-pure gem, which provides a pure Ruby YAML
181
+ parser with comment preservation.
182
+
183
+ ## License
184
+
185
+ MIT
data/bin/yaml-janitor ADDED
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/yaml_janitor"
5
+
6
+ def print_usage
7
+ puts <<~USAGE
8
+ Usage: yaml-janitor [options] <file_or_directory>
9
+
10
+ Options:
11
+ --fix Auto-fix issues where possible
12
+ --rules RULES Comma-separated list of rules (default: all)
13
+ --config PATH Path to config file (default: .yaml-janitor.yml)
14
+ --indentation N Number of spaces for indentation (default: 2)
15
+ --line-width N Maximum line width (default: 80)
16
+ --help Show this help message
17
+
18
+ Examples:
19
+ yaml-janitor config.yml
20
+ yaml-janitor --fix config.yml
21
+ yaml-janitor --rules multiline_certificate containers/
22
+ yaml-janitor --config my-config.yml --fix config.yml
23
+ yaml-janitor --indentation 4 --line-width 100 config.yml
24
+ USAGE
25
+ exit 0
26
+ end
27
+
28
+ # Parse args
29
+ fix = false
30
+ rules = :all
31
+ config_path = nil
32
+ config_overrides = {}
33
+ paths = []
34
+
35
+ i = 0
36
+ while i < ARGV.length
37
+ case ARGV[i]
38
+ when "--fix"
39
+ fix = true
40
+ when "--rules"
41
+ i += 1
42
+ rules = ARGV[i].split(",").map(&:to_sym)
43
+ when "--config"
44
+ i += 1
45
+ config_path = ARGV[i]
46
+ when "--indentation"
47
+ i += 1
48
+ config_overrides[:indentation] = ARGV[i].to_i
49
+ when "--line-width"
50
+ i += 1
51
+ config_overrides[:line_width] = ARGV[i].to_i
52
+ when "--help", "-h"
53
+ print_usage
54
+ else
55
+ paths << ARGV[i]
56
+ end
57
+ i += 1
58
+ end
59
+
60
+ if paths.empty?
61
+ puts "Error: No files or directories specified"
62
+ print_usage
63
+ end
64
+
65
+ # Look for default config if not specified
66
+ if !config_path && File.exist?(".yaml-janitor.yml")
67
+ config_path = ".yaml-janitor.yml"
68
+ end
69
+
70
+ # Process files
71
+ config = YamlJanitor::Config.new(config_path: config_path, overrides: config_overrides)
72
+ linter = YamlJanitor::Linter.new(rules: rules, config: config)
73
+ total_files = 0
74
+ total_violations = 0
75
+ files_with_violations = []
76
+
77
+ paths.each do |path|
78
+ if File.directory?(path)
79
+ # Process all YAML files in directory
80
+ yaml_files = Dir.glob(File.join(path, "**", "*.{yml,yaml}"))
81
+ yaml_files.each do |file|
82
+ total_files += 1
83
+ result = linter.lint_file(file, fix: fix)
84
+
85
+ if result[:violations].any?
86
+ files_with_violations << file
87
+ total_violations += result[:violations].length
88
+
89
+ puts "\n#{file}:"
90
+ result[:violations].each do |violation|
91
+ puts " #{violation}"
92
+ end
93
+
94
+ if fix && result[:fixed]
95
+ puts " ✓ Fixed"
96
+ end
97
+ end
98
+ end
99
+ elsif File.file?(path)
100
+ # Process single file
101
+ total_files += 1
102
+ result = linter.lint_file(path, fix: fix)
103
+
104
+ if result[:violations].any?
105
+ files_with_violations << path
106
+ total_violations += result[:violations].length
107
+
108
+ puts "\n#{path}:"
109
+ result[:violations].each do |violation|
110
+ puts " #{violation}"
111
+ end
112
+
113
+ if fix && result[:fixed]
114
+ puts " ✓ Fixed"
115
+ end
116
+ end
117
+ else
118
+ puts "Warning: #{path} not found"
119
+ end
120
+ end
121
+
122
+ # Summary
123
+ puts "\n" + "="*60
124
+ puts "Checked #{total_files} files"
125
+ puts "Found #{total_violations} violations in #{files_with_violations.length} files"
126
+
127
+ if total_violations > 0
128
+ exit 1
129
+ else
130
+ puts "✓ All files clean!"
131
+ exit 0
132
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'yaml'
4
+
5
+ module YamlJanitor
6
+ class Config
7
+ DEFAULT_CONFIG = {
8
+ indentation: 2,
9
+ line_width: 80,
10
+ sequence_indent: false,
11
+ rules: {
12
+ multiline_certificate: { enabled: true },
13
+ consistent_indentation: { enabled: true }
14
+ }
15
+ }.freeze
16
+
17
+ attr_reader :config
18
+
19
+ def initialize(config_path: nil, overrides: {})
20
+ @config = deep_dup(DEFAULT_CONFIG)
21
+ load_config_file(config_path) if config_path
22
+ merge_overrides(overrides)
23
+ end
24
+
25
+ def indentation
26
+ @config[:indentation]
27
+ end
28
+
29
+ def line_width
30
+ @config[:line_width]
31
+ end
32
+
33
+ def sequence_indent
34
+ @config[:sequence_indent]
35
+ end
36
+
37
+ def rule_enabled?(rule_name)
38
+ rule_config = @config[:rules][rule_name.to_sym]
39
+ rule_config && rule_config[:enabled] != false
40
+ end
41
+
42
+ def rule_config(rule_name)
43
+ @config[:rules][rule_name.to_sym] || {}
44
+ end
45
+
46
+ def dump_options
47
+ {
48
+ indentation: indentation,
49
+ line_width: line_width,
50
+ sequence_indent: sequence_indent
51
+ }
52
+ end
53
+
54
+ private
55
+
56
+ def load_config_file(path)
57
+ return unless File.exist?(path)
58
+
59
+ file_config = YAML.safe_load(File.read(path), symbolize_names: true)
60
+ deep_merge!(@config, file_config)
61
+ rescue => e
62
+ warn "Warning: Could not load config file #{path}: #{e.message}"
63
+ end
64
+
65
+ def merge_overrides(overrides)
66
+ deep_merge!(@config, overrides)
67
+ end
68
+
69
+ def deep_dup(hash)
70
+ hash.each_with_object({}) do |(key, value), result|
71
+ result[key] = value.is_a?(Hash) ? deep_dup(value) : value
72
+ end
73
+ end
74
+
75
+ def deep_merge!(hash, other_hash)
76
+ other_hash.each do |key, value|
77
+ if value.is_a?(Hash) && hash[key].is_a?(Hash)
78
+ deep_merge!(hash[key], value)
79
+ else
80
+ hash[key] = value
81
+ end
82
+ end
83
+ hash
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YamlJanitor
4
+ class Linter
5
+ attr_reader :rules
6
+
7
+ def initialize(rules: :all, config: nil, config_path: nil)
8
+ @config = config || Config.new(config_path: config_path)
9
+ @rules = load_rules(rules)
10
+ end
11
+
12
+ # Lint a file, optionally fixing issues
13
+ def lint_file(path, fix: false)
14
+ yaml_content = File.read(path)
15
+ result = lint(yaml_content, fix: fix, file: path)
16
+
17
+ if fix && result[:fixed]
18
+ File.write(path, result[:output])
19
+ end
20
+
21
+ result
22
+ end
23
+
24
+ # Lint YAML content, optionally fixing issues
25
+ def lint(yaml_content, fix: false, file: nil)
26
+ violations = []
27
+
28
+ # Load with comments
29
+ loaded = Psych::Pure.load(yaml_content, comments: true)
30
+
31
+ # Check for violations
32
+ @rules.each do |rule|
33
+ violations += rule.check(loaded, file: file)
34
+ end
35
+
36
+ # Apply fixes if requested
37
+ output = yaml_content
38
+ fixed = false
39
+
40
+ if fix && violations.any?
41
+ @rules.each do |rule|
42
+ rule.fix!(loaded)
43
+ end
44
+
45
+ # Dump back to YAML with configured options
46
+ output = Psych::Pure.dump(loaded, **@config.dump_options)
47
+ fixed = true
48
+
49
+ # Paranoid mode: verify semantics match
50
+ verify_semantics!(yaml_content, output)
51
+ end
52
+
53
+ {
54
+ violations: violations,
55
+ fixed: fixed,
56
+ output: output
57
+ }
58
+ rescue => e
59
+ {
60
+ violations: [Violation.new(
61
+ rule: :parse_error,
62
+ message: e.message,
63
+ file: file
64
+ )],
65
+ fixed: false,
66
+ output: yaml_content,
67
+ error: e
68
+ }
69
+ end
70
+
71
+ private
72
+
73
+ def load_rules(rule_specs)
74
+ available_rules = {
75
+ multiline_certificate: Rules::MultilineCertificate,
76
+ consistent_indentation: Rules::ConsistentIndentation
77
+ }
78
+
79
+ if rule_specs == :all
80
+ # Load all enabled rules from config
81
+ rule_names = available_rules.keys.select do |name|
82
+ @config.rule_enabled?(name)
83
+ end
84
+ elsif rule_specs.is_a?(Array)
85
+ rule_names = rule_specs
86
+ else
87
+ raise Error, "Invalid rules specification: #{rule_specs.inspect}"
88
+ end
89
+
90
+ rule_names.map do |name|
91
+ rule_class = available_rules[name.to_sym]
92
+ raise Error, "Unknown rule: #{name}" unless rule_class
93
+ next unless @config.rule_enabled?(name)
94
+
95
+ rule_class.new(@config.rule_config(name))
96
+ end.compact
97
+ end
98
+
99
+ def verify_semantics!(original, fixed)
100
+ original_data = YAML.load(original)
101
+ fixed_data = YAML.load(fixed)
102
+
103
+ if original_data != fixed_data
104
+ raise SemanticMismatchError, "Fixed YAML has different semantics than original"
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YamlJanitor
4
+ class Rule
5
+ def initialize(config = {})
6
+ @config = config
7
+ end
8
+ # Check for violations in the loaded YAML structure
9
+ # Returns an array of Violation objects
10
+ def check(loaded, file: nil)
11
+ violations = []
12
+ walk(loaded) do |node, path|
13
+ if violation = check_node(node, path)
14
+ violations << Violation.new(
15
+ rule: rule_name,
16
+ message: violation,
17
+ file: file
18
+ )
19
+ end
20
+ end
21
+ violations
22
+ end
23
+
24
+ # Fix violations in the loaded YAML structure
25
+ # Modifies the structure in place
26
+ def fix!(loaded)
27
+ walk(loaded) do |node, path|
28
+ fix_node(node, path)
29
+ end
30
+ end
31
+
32
+ # Override this to check individual nodes
33
+ def check_node(node, path)
34
+ nil
35
+ end
36
+
37
+ # Override this to fix individual nodes
38
+ def fix_node(node, path)
39
+ # No-op by default
40
+ end
41
+
42
+ # Override this to provide the rule name
43
+ def rule_name
44
+ self.class.name.split("::").last.downcase
45
+ end
46
+
47
+ private
48
+
49
+ # Walk the YAML structure, yielding each node with its path
50
+ def walk(node, path = [], &block)
51
+ yield node, path
52
+
53
+ # Handle both regular and LoadedHash/LoadedArray from psych-pure
54
+ if hash_like?(node)
55
+ node.each do |key, value|
56
+ walk(value, path + [key], &block)
57
+ end
58
+ elsif array_like?(node)
59
+ node.each_with_index do |value, index|
60
+ walk(value, path + [index], &block)
61
+ end
62
+ end
63
+ end
64
+
65
+ def hash_like?(node)
66
+ node.is_a?(Hash) || node.class.name == 'Psych::Pure::LoadedHash'
67
+ end
68
+
69
+ def array_like?(node)
70
+ node.is_a?(Array) || node.class.name == 'Psych::Pure::LoadedArray'
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YamlJanitor
4
+ module Rules
5
+ class ConsistentIndentation < Rule
6
+ def rule_name
7
+ "consistent_indentation"
8
+ end
9
+
10
+ # This rule checks the original YAML source for inconsistent indentation
11
+ # The fix is automatic via Psych::Pure.dump with configured indentation
12
+ def check(loaded, file: nil)
13
+ return [] unless file
14
+
15
+ # Read the original source
16
+ source = File.read(file)
17
+
18
+ violations = []
19
+ indentation_levels = detect_indentation_levels(source)
20
+
21
+ if indentation_levels.length > 1
22
+ violations << Violation.new(
23
+ rule: rule_name,
24
+ message: "Inconsistent indentation detected: #{indentation_levels.sort.join(', ')} spaces used",
25
+ file: file
26
+ )
27
+ end
28
+
29
+ violations
30
+ end
31
+
32
+ # Fix is automatic - Psych::Pure.dump will use configured indentation
33
+ def fix!(loaded)
34
+ # No-op - the dumper handles this via config.dump_options
35
+ end
36
+
37
+ private
38
+
39
+ def detect_indentation_levels(source)
40
+ # Track the indentation increment between parent and child
41
+ indents = []
42
+ prev_indent = 0
43
+
44
+ source.each_line do |line|
45
+ # Skip empty lines, comments, and document markers
46
+ next if line.strip.empty?
47
+ next if line.strip.start_with?('#')
48
+ next if line.strip.start_with?('---')
49
+ next if line.strip.start_with?('...')
50
+ next unless line.include?(':') # Only look at key lines
51
+
52
+ # Count leading spaces
53
+ spaces = 0
54
+ spaces = line[/^ +/].length if line.start_with?(' ')
55
+
56
+ # Calculate the indent increment from previous level
57
+ if spaces > prev_indent
58
+ indent_increment = spaces - prev_indent
59
+ indents << indent_increment
60
+ end
61
+
62
+ prev_indent = spaces if line.strip.end_with?(':')
63
+ end
64
+
65
+ # Find unique indentation increments
66
+ indents.uniq
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YamlJanitor
4
+ module Rules
5
+ class MultilineCertificate < Rule
6
+ CERT_PATTERNS = [
7
+ /BEGIN CERTIFICATE/,
8
+ /BEGIN RSA PRIVATE KEY/,
9
+ /BEGIN PRIVATE KEY/,
10
+ /BEGIN PUBLIC KEY/
11
+ ].freeze
12
+
13
+ def rule_name
14
+ "multiline_certificate"
15
+ end
16
+
17
+ def check_node(node, path)
18
+ return unless node.is_a?(String)
19
+ return unless contains_certificate?(node)
20
+ return unless has_embedded_newlines?(node)
21
+
22
+ key = path.last
23
+ "Key '#{key}' contains a multi-line certificate in double-quoted format (causes psych-pure bug)"
24
+ end
25
+
26
+ def fix_node(node, path)
27
+ return unless node.is_a?(String)
28
+ return unless contains_certificate?(node)
29
+ return unless has_embedded_newlines?(node)
30
+
31
+ # For LoadedObject (psych-pure's wrapper), we need to modify the underlying string
32
+ if node.respond_to?(:__getobj__)
33
+ underlying = node.__getobj__
34
+ # Mark as dirty if it's a LoadedObject
35
+ node.instance_variable_set(:@dirty, true) if node.respond_to?(:instance_variable_set)
36
+ else
37
+ underlying = node
38
+ end
39
+
40
+ # Can't actually fix this yet - psych-pure doesn't support setting style hints on strings
41
+ # This would need psych-pure to support something like:
42
+ # node.psych_style = :literal
43
+ #
44
+ # For now, just detect the issue
45
+ end
46
+
47
+ private
48
+
49
+ def contains_certificate?(string)
50
+ CERT_PATTERNS.any? { |pattern| string.match?(pattern) }
51
+ end
52
+
53
+ def has_embedded_newlines?(string)
54
+ string.include?("\n")
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YamlJanitor
4
+ VERSION = "20251113"
5
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module YamlJanitor
4
+ class Violation
5
+ attr_reader :rule, :message, :line, :column, :file
6
+
7
+ def initialize(rule:, message:, line: nil, column: nil, file: nil)
8
+ @rule = rule
9
+ @message = message
10
+ @line = line
11
+ @column = column
12
+ @file = file
13
+ end
14
+
15
+ def to_s
16
+ location = []
17
+ location << file if file
18
+ location << "line #{line}" if line
19
+ location << "column #{column}" if column
20
+
21
+ prefix = location.empty? ? "" : "#{location.join(":")} - "
22
+ "#{prefix}[#{rule}] #{message}"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "psych/pure"
4
+ require "yaml"
5
+
6
+ require_relative "yaml_janitor/version"
7
+ require_relative "yaml_janitor/config"
8
+ require_relative "yaml_janitor/linter"
9
+ require_relative "yaml_janitor/rule"
10
+ require_relative "yaml_janitor/violation"
11
+ require_relative "yaml_janitor/rules/multiline_certificate"
12
+ require_relative "yaml_janitor/rules/consistent_indentation"
13
+
14
+ module YamlJanitor
15
+ class Error < StandardError; end
16
+
17
+ class SemanticMismatchError < Error; end
18
+
19
+ class << self
20
+ # Convenience method to lint a file
21
+ def lint_file(path, rules: :all, fix: false)
22
+ linter = Linter.new(rules: rules)
23
+ linter.lint_file(path, fix: fix)
24
+ end
25
+
26
+ # Convenience method to lint a string
27
+ def lint(yaml_string, rules: :all, fix: false)
28
+ linter = Linter.new(rules: rules)
29
+ linter.lint(yaml_string, fix: fix)
30
+ end
31
+ end
32
+ end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: yaml-janitor
3
+ version: !ruby/object:Gem::Version
4
+ version: '20251113'
5
+ platform: ruby
6
+ authors:
7
+ - ducks
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-11-13 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: psych-pure
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '5.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '5.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '13.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '13.0'
55
+ description: A YAML linter built on psych-pure that can detect and fix common issues
56
+ while preserving comments
57
+ email:
58
+ - ducks@discourse.org
59
+ executables:
60
+ - yaml-janitor
61
+ extensions: []
62
+ extra_rdoc_files: []
63
+ files:
64
+ - LICENSE
65
+ - README.md
66
+ - bin/yaml-janitor
67
+ - lib/yaml_janitor.rb
68
+ - lib/yaml_janitor/config.rb
69
+ - lib/yaml_janitor/linter.rb
70
+ - lib/yaml_janitor/rule.rb
71
+ - lib/yaml_janitor/rules/consistent_indentation.rb
72
+ - lib/yaml_janitor/rules/multiline_certificate.rb
73
+ - lib/yaml_janitor/version.rb
74
+ - lib/yaml_janitor/violation.rb
75
+ homepage: https://github.com/ducks/yaml-janitor
76
+ licenses:
77
+ - MIT
78
+ metadata: {}
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: 3.2.0
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubygems_version: 3.5.22
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: YAML linter that preserves comments using psych-pure
98
+ test_files: []