Skip to content

Commit 655459a

Browse files
committed
✨ stdlib-only config seeder
1 parent a6a937b commit 655459a

5 files changed

Lines changed: 140 additions & 59 deletions

File tree

lib/kettle/jem.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ class Error < StandardError; end
6767
autoload :TemplatingReport, "kettle/jem/templating_report"
6868

6969
# Templating and setup (moved from kettle-dev)
70+
autoload :ConfigSeeder, "kettle/jem/config_seeder"
7071
autoload :TemplateHelpers, "kettle/jem/template_helpers"
7172
autoload :ModularGemfiles, "kettle/jem/modular_gemfiles"
7273
autoload :SetupCLI, "kettle/jem/setup_cli"

lib/kettle/jem/config_seeder.rb

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# frozen_string_literal: true
2+
3+
require "yaml"
4+
5+
module Kettle
6+
module Jem
7+
# Bootstrap-safe helpers for backfilling .kettle-jem.yml token values.
8+
#
9+
# This module intentionally depends only on stdlib so it can be used from
10+
# the standalone executable before the full bundled runtime is available.
11+
module ConfigSeeder
12+
TOKEN_PLACEHOLDER_RE = /\{KJ\|[^}]+}/.freeze
13+
INLINE_ENV_RE = /ENV:\s*(KJ_[A-Z0-9_]+)\b/.freeze
14+
15+
module_function
16+
17+
def seed_kettle_config_content(content, token_values, env: ENV)
18+
token_values ||= {}
19+
20+
updated_content, = backfill_kettle_config_token_lines(content.to_s, token_values, env: env)
21+
updated_content
22+
end
23+
24+
def placeholder_or_blank_kettle_config_scalar?(raw_value)
25+
stripped = raw_value.to_s.strip
26+
return true if stripped.empty?
27+
28+
parsed = begin
29+
YAML.safe_load(stripped, permitted_classes: [], aliases: false)
30+
rescue StandardError
31+
stripped.delete_prefix('"').delete_suffix('"').delete_prefix("'").delete_suffix("'")
32+
end
33+
34+
value = parsed.is_a?(String) ? parsed : parsed.to_s
35+
value.to_s.strip.empty? || token_placeholder?(value)
36+
end
37+
38+
def yaml_scalar_for_kettle_config_backfill(value, current_raw)
39+
stripped = current_raw.to_s.strip
40+
if stripped.start_with?("'") && stripped.end_with?("'")
41+
"'#{value.to_s.gsub("'", "''")}'"
42+
else
43+
value.to_s.dump
44+
end
45+
end
46+
47+
def backfill_kettle_config_token_lines(content, token_values, env: ENV)
48+
in_tokens = false
49+
current_section = nil
50+
changed = false
51+
52+
updated = content.lines.map do |line|
53+
stripped = line.lstrip
54+
indent = line[/\A\s*/].to_s.length
55+
56+
if indent.zero? && stripped.match?(/\Atokens:\s*(?:#.*)?\z/)
57+
in_tokens = true
58+
current_section = nil
59+
next line
60+
elsif indent.zero? && stripped.match?(/\A[\w-]+:\s*(?:#.*)?\z/)
61+
in_tokens = false
62+
current_section = nil
63+
end
64+
65+
next line unless in_tokens
66+
67+
if indent == 2 && (match = stripped.match(/\A([a-z_]+):\s*(?:#.*)?\z/))
68+
current_section = match[1]
69+
next line
70+
end
71+
72+
next line unless indent == 4 && current_section
73+
74+
match = line.match(/\A(\s*)([a-z_]+):(\s*)([^#\n]*?)(\s*(?:#.*)?)?(\n?)\z/)
75+
next line unless match
76+
77+
key = match[2]
78+
desired_value = token_values.dig(current_section, key)
79+
desired_value = env[inline_env_key(match[5])] if !present_string?(desired_value) && inline_env_key(match[5])
80+
next line unless present_string?(desired_value)
81+
next line unless placeholder_or_blank_kettle_config_scalar?(match[4])
82+
83+
changed = true
84+
"#{match[1]}#{key}:#{match[3]}#{yaml_scalar_for_kettle_config_backfill(desired_value, match[4])}#{match[5]}#{match[6]}"
85+
end.join
86+
87+
[updated, changed]
88+
end
89+
90+
def inline_env_key(comment)
91+
comment.to_s[INLINE_ENV_RE, 1]
92+
end
93+
94+
def present_string?(value)
95+
!value.to_s.strip.empty?
96+
end
97+
98+
def token_placeholder?(value)
99+
value.to_s.match?(TOKEN_PLACEHOLDER_RE)
100+
end
101+
end
102+
end
103+
end

lib/kettle/jem/setup_cli.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
require "open3"
66
require "optparse"
77

8+
require_relative "config_seeder"
9+
810
module Kettle
911
module Jem
1012
# SetupCLI bootstraps a host gem repository to use kettle-jem tooling.
@@ -407,7 +409,7 @@ def ensure_template_config_bootstrap!
407409
end
408410

409411
def seed_bootstrap_template_config(content)
410-
Kettle::Jem::TemplateHelpers.seed_kettle_config_content(content, bootstrap_template_config_values)
412+
Kettle::Jem::ConfigSeeder.seed_kettle_config_content(content, bootstrap_template_config_values)
411413
end
412414

413415
def bootstrap_template_config_values

lib/kettle/jem/template_helpers.rb

Lines changed: 8 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
require "set"
66
require "yaml"
77

8+
require_relative "config_seeder"
9+
810
module Kettle
911
module Jem
1012
# Helpers shared by kettle:jem Rake tasks for templating and file ops.
@@ -369,75 +371,23 @@ def derived_token_config_values
369371
end
370372

371373
def seed_kettle_config_content(content, token_values)
372-
return content.to_s if token_values.nil? || token_values.empty?
374+
token_values ||= {}
375+
updated_content = Kettle::Jem::ConfigSeeder.seed_kettle_config_content(content.to_s, token_values)
376+
return updated_content if token_values.empty?
373377

374-
updated_content, = backfill_kettle_config_token_lines(content.to_s, token_values)
375378
merge_missing_kettle_config_token_values(updated_content, token_values)
376379
end
377380

378381
def placeholder_or_blank_kettle_config_scalar?(raw_value)
379-
stripped = raw_value.to_s.strip
380-
return true if stripped.empty?
381-
382-
parsed = begin
383-
YAML.safe_load(stripped, permitted_classes: [], aliases: false)
384-
rescue StandardError
385-
stripped.delete_prefix('"').delete_suffix('"').delete_prefix("'").delete_suffix("'")
386-
end
387-
388-
value = parsed.is_a?(String) ? parsed : parsed.to_s
389-
value.to_s.strip.empty? || token_placeholder?(value)
382+
Kettle::Jem::ConfigSeeder.placeholder_or_blank_kettle_config_scalar?(raw_value)
390383
end
391384

392385
def yaml_scalar_for_kettle_config_backfill(value, current_raw)
393-
stripped = current_raw.to_s.strip
394-
if stripped.start_with?("'") && stripped.end_with?("'")
395-
"'#{value.to_s.gsub("'", "''")}'"
396-
else
397-
value.to_s.dump
398-
end
386+
Kettle::Jem::ConfigSeeder.yaml_scalar_for_kettle_config_backfill(value, current_raw)
399387
end
400388

401389
def backfill_kettle_config_token_lines(content, token_values)
402-
in_tokens = false
403-
current_section = nil
404-
changed = false
405-
406-
updated = content.lines.map do |line|
407-
stripped = line.lstrip
408-
indent = line[/\A\s*/].to_s.length
409-
410-
if indent.zero? && stripped.match?(/\Atokens:\s*(?:#.*)?\z/)
411-
in_tokens = true
412-
current_section = nil
413-
next line
414-
elsif indent.zero? && stripped.match?(/\A[\w-]+:\s*(?:#.*)?\z/)
415-
in_tokens = false
416-
current_section = nil
417-
end
418-
419-
next line unless in_tokens
420-
421-
if indent == 2 && (match = stripped.match(/\A([a-z_]+):\s*(?:#.*)?\z/))
422-
current_section = match[1]
423-
next line
424-
end
425-
426-
next line unless indent == 4 && current_section
427-
428-
match = line.match(/\A(\s*)([a-z_]+):(\s*)([^#\n]*?)(\s*(?:#.*)?)?(\n?)\z/)
429-
next line unless match
430-
431-
key = match[2]
432-
desired_value = token_values.dig(current_section, key)
433-
next line unless present_string?(desired_value)
434-
next line unless placeholder_or_blank_kettle_config_scalar?(match[4])
435-
436-
changed = true
437-
"#{match[1]}#{key}:#{match[3]}#{yaml_scalar_for_kettle_config_backfill(desired_value, match[4])}#{match[5]}#{match[6]}"
438-
end.join
439-
440-
[updated, changed]
390+
Kettle::Jem::ConfigSeeder.backfill_kettle_config_token_lines(content, token_values)
441391
end
442392

443393
def merge_missing_kettle_config_token_values(destination_content, token_values)

spec/kettle/jem/exe_load_error_spec.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# frozen_string_literal: true
22

3+
require "open3"
4+
require "rbconfig"
5+
36
require "spec_helper"
47

58
RSpec.describe "exe/kettle-jem bootstrap loading" do # rubocop:disable RSpec/DescribeClass
@@ -15,4 +18,26 @@
1518
expect(exe_content).not_to include('require "kettle/jem"')
1619
expect(exe_content).not_to include('require "psych-merge"')
1720
end
21+
22+
it "can seed bootstrap config after loading only setup_cli and version" do
23+
version_path = File.expand_path("../../../lib/kettle/jem/version", __dir__)
24+
setup_cli_path = File.expand_path("../../../lib/kettle/jem/setup_cli", __dir__)
25+
template_path = File.expand_path("../../../template/.kettle-jem.yml.example", __dir__)
26+
script = <<~RUBY
27+
require "yaml"
28+
require_relative #{version_path.inspect}
29+
require_relative #{setup_cli_path.inspect}
30+
31+
cli = Kettle::Jem::SetupCLI.allocate
32+
seeded = cli.send(:seed_bootstrap_template_config, File.read(#{template_path.inspect}))
33+
parsed = YAML.safe_load(seeded, permitted_classes: [], aliases: false)
34+
abort("expected gh_user to be seeded") unless parsed.dig("tokens", "forge", "gh_user") == "pboling"
35+
RUBY
36+
37+
stdout, stderr, status = Open3.capture3({"KJ_GH_USER" => "pboling"}, RbConfig.ruby, "-e", script)
38+
39+
expect(status.success?).to be(true), "stdout=#{stdout.inspect}\nstderr=#{stderr.inspect}"
40+
expect(stdout).to eq("")
41+
expect(stderr).to eq("")
42+
end
1843
end

0 commit comments

Comments
 (0)