|
| 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 |
0 commit comments