Class: Ast::Merge::Recipe::Config

Inherits:
Preset
  • Object
show all
Defined in:
lib/ast/merge/recipe/config.rb

Overview

Loads and represents a merge recipe from YAML configuration.

A recipe extends Preset with:

  • Optional template file specification
  • Optional target file patterns
  • Injection point configuration
  • when_missing behavior

File-oriented recipes provide a template path and targets for on-disk execution via Runner#run. Content-oriented recipes may omit both and be executed in memory via Runner#run_content.

Examples:

Loading a file-oriented recipe

recipe = Config.load(".merge-recipes/gem_family_section.yml")
recipe.name          # => "gem_family_section"
recipe.template_path # => "GEM_FAMILY_SECTION.md"
recipe.targets       # => ["README.md", "vendor/*/README.md"]

Recipe YAML format

name: gem_family_section
description: Update gem family section in README files

template: GEM_FAMILY_SECTION.md

targets:
  - "README.md"
  - "vendor/*/README.md"

injection:
  anchor:
    type: heading
    text: /Gem Family/
  position: replace
  boundary:
    type: heading
    same_or_shallower: true

merge:
  preference: template
  add_missing: true

when_missing: skip

See Also:

Instance Attribute Summary collapse

Attributes inherited from Preset

#description, #freeze_token, #merge_config, #name, #parser, #parser_explicit, #preset_path

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Preset

#add_missing, #add_missing?, #match_refiner, #node_typing, #normalize_whitespace, #parser_explicit?, #preference, #rehydrate_link_references, #script_loader, #signature_generator, #to_h

Constructor Details

#initialize(config, preset_path: nil, recipe_path: nil) ⇒ Config

Create a recipe from a hash (parsed YAML or programmatic).

Parameters:

  • config (Hash)

    Recipe configuration

  • preset_path (String, nil) (defaults to: nil)

    Path to recipe file (for relative path resolution)

  • recipe_path (String, nil) (defaults to: nil)

    Alias for preset_path (backward compatibility)



94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/ast/merge/recipe/config.rb', line 94

def initialize(config, preset_path: nil, recipe_path: nil)
  # Support both preset_path and recipe_path for backward compatibility
  effective_path = preset_path || recipe_path
  super(config, preset_path: effective_path)

  @template_path = config['template']
  @targets = Array(config.fetch('targets', default_targets_for(@template_path)))
  @when_missing = (config['when_missing'] || 'skip').to_sym
  validate_top_level_step_contract!(config)
  @injection = parse_injection(config['injection'] || {})
  @steps = parse_steps(config['steps'])
end

Instance Attribute Details

#injectionHash (readonly)

Returns Injection point / partial target configuration.

Returns:

  • (Hash)

    Injection point / partial target configuration



62
63
64
# File 'lib/ast/merge/recipe/config.rb', line 62

def injection
  @injection
end

#stepsArray<Hash> (readonly)

Returns Normalized execution steps.

Returns:

  • (Array<Hash>)

    Normalized execution steps



68
69
70
# File 'lib/ast/merge/recipe/config.rb', line 68

def steps
  @steps
end

#targetsArray<String> (readonly)

Returns Glob patterns for target files.

Returns:

  • (Array<String>)

    Glob patterns for target files



59
60
61
# File 'lib/ast/merge/recipe/config.rb', line 59

def targets
  @targets
end

#template_pathString? (readonly)

Returns Path to template file (relative to recipe or absolute).

Returns:

  • (String, nil)

    Path to template file (relative to recipe or absolute)



56
57
58
# File 'lib/ast/merge/recipe/config.rb', line 56

def template_path
  @template_path
end

#when_missingSymbol (readonly)

Returns Behavior when the partial target is not found (:skip, :append, :prepend, :add).

Returns:

  • (Symbol)

    Behavior when the partial target is not found (:skip, :append, :prepend, :add)



65
66
67
# File 'lib/ast/merge/recipe/config.rb', line 65

def when_missing
  @when_missing
end

Class Method Details

.load(path) ⇒ Config

Load a recipe from a YAML file.

Parameters:

  • path (String)

    Path to the recipe YAML file

Returns:

Raises:

  • (ArgumentError)

    If file doesn't exist or is invalid



81
82
83
84
85
86
# File 'lib/ast/merge/recipe/config.rb', line 81

def load(path)
  raise ArgumentError, "Recipe file not found: #{path}" unless File.exist?(path)

  yaml = YAML.safe_load_file(path, permitted_classes: [Regexp, Symbol])
  new(yaml, preset_path: path)
end

Instance Method Details

#content_recipe?Boolean

Returns Whether this recipe expects caller-provided content.

Returns:

  • (Boolean)

    Whether this recipe expects caller-provided content.



234
235
236
# File 'lib/ast/merge/recipe/config.rb', line 234

def content_recipe?
  !file_recipe?
end

#execution_stepsArray<Hash>

Returns Explicit steps or a synthesized legacy-compatible step.

Returns:

  • (Array<Hash>)

    Explicit steps or a synthesized legacy-compatible step



215
216
217
218
219
# File 'lib/ast/merge/recipe/config.rb', line 215

def execution_steps
  return steps unless steps.empty?

  [legacy_implicit_step]
end

#expand_targets(base_dir: nil) ⇒ Array<String>

Expand target globs to actual file paths.

Parameters:

  • base_dir (String) (defaults to: nil)

    Base directory for glob expansion

Returns:

  • (Array<String>)

    Absolute paths to target files



123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/ast/merge/recipe/config.rb', line 123

def expand_targets(base_dir: nil)
  return [] if targets.empty?

  base = base_dir || (preset_path ? File.dirname(preset_path) : Dir.pwd)

  targets.flat_map do |pattern|
    if File.absolute_path?(pattern)
      Dir.glob(pattern)
    else
      # Expand and normalize to remove .. segments
      expanded_pattern = File.expand_path(pattern, base)
      Dir.glob(expanded_pattern)
    end
  end.uniq.sort
end

#explicit_steps?Boolean

Returns Whether this recipe uses explicit step execution.

Returns:

  • (Boolean)

    Whether this recipe uses explicit step execution



210
211
212
# File 'lib/ast/merge/recipe/config.rb', line 210

def explicit_steps?
  !@steps.empty?
end

#file_recipe?Boolean

Returns Whether this recipe can be executed against files on disk.

Returns:

  • (Boolean)

    Whether this recipe can be executed against files on disk.



229
230
231
# File 'lib/ast/merge/recipe/config.rb', line 229

def file_recipe?
  !template_path.nil?
end

#finder_queryHash

Build an InjectionPointFinder query from the injection config.

Returns:

  • (Hash)

    Arguments for InjectionPointFinder#find



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/ast/merge/recipe/config.rb', line 142

def finder_query
  return {} unless navigable_partial_target?

  anchor = injection[:anchor] || {}
  boundary = injection[:boundary] || {}

  query = {
    type: anchor[:type],
    text: anchor[:text],
    position: injection[:position] || :replace,
    boundary_type: boundary[:type],
    boundary_text: boundary[:text]
  }

  # Support tree-depth based boundary detection
  # same_or_shallower: true means "end at next sibling (same tree level or above)"
  query[:boundary_same_or_shallower] = true if boundary[:same_or_shallower]

  query.compact
end

#key_path_partial_target?Boolean

Returns:

  • (Boolean)


205
206
207
# File 'lib/ast/merge/recipe/config.rb', line 205

def key_path_partial_target?
  partial_target_kind == :key_path
end

Returns:

  • (Boolean)


200
201
202
# File 'lib/ast/merge/recipe/config.rb', line 200

def navigable_partial_target?
  partial_target_kind == :navigable
end

#partial_targetHash?

Get the normalized partial-target contract for this recipe.

This is the shared shape used by the stock runner to dispatch between parser families without baking parser-specific YAML parsing logic into the runner itself.

Returns:

  • (Hash, nil)


170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/ast/merge/recipe/config.rb', line 170

def partial_target
  return if explicit_steps?

  case partial_target_kind
  when :navigable
    {
      kind: :navigable,
      anchor: injection[:anchor],
      position: injection[:position] || :replace,
      boundary: injection[:boundary]
    }.compact
  when :key_path
    {
      kind: :key_path,
      key_path: injection[:key_path]
    }
  end
end

#partial_target_kindSymbol?

Returns The normalized target selector kind.

Returns:

  • (Symbol, nil)

    The normalized target selector kind



190
191
192
193
194
195
196
197
# File 'lib/ast/merge/recipe/config.rb', line 190

def partial_target_kind
  return if explicit_steps?

  return :key_path if injection[:key_path]
  return :navigable if injection[:anchor]

  nil
end

#recipe_pathObject

Alias for compatibility - recipe_path points to the same file as preset_path



71
72
73
# File 'lib/ast/merge/recipe/config.rb', line 71

def recipe_path
  preset_path
end

#replace_mode?Boolean

Whether to use replace mode (template replaces section entirely).

Returns:

  • (Boolean)


224
225
226
# File 'lib/ast/merge/recipe/config.rb', line 224

def replace_mode?
  merge_config[:replace_mode] == true
end

#template_absolute_path(base_dir: nil) ⇒ String

Get the absolute path to the template file.

Parameters:

  • base_dir (String) (defaults to: nil)

    Base directory for relative paths

Returns:

  • (String)

    Absolute path to template



111
112
113
114
115
116
117
# File 'lib/ast/merge/recipe/config.rb', line 111

def template_absolute_path(base_dir: nil)
  return if @template_path.nil?
  return @template_path if File.absolute_path?(@template_path)

  base = base_dir || (preset_path ? File.dirname(preset_path) : Dir.pwd)
  File.expand_path(@template_path, base)
end