Skip to content
146 changes: 146 additions & 0 deletions lib/openvox-strings/hiera.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# frozen_string_literal: true

require 'yaml'

module OpenvoxStrings
# Parser for Hiera configuration and data
class Hiera
attr_reader :hiera_config, :common_data

# Initializes a Hiera parser for a given module path
# @param [String] module_path The path to the Puppet module root directory
def initialize(module_path)
@module_path = module_path
@hiera_config = load_hiera_config
@common_data = load_common_data
end

# Checks if Hiera is configured for this module
# @return [Boolean] true if hiera.yaml exists and is valid
def hiera_enabled?
!@hiera_config.nil?
end

# Gets the default value for a parameter from Hiera data
# @param [String] class_name The fully qualified class name (e.g., 'github_actions_runner')
# @param [String] param_name The parameter name
# @return [String, nil] The default value as a string, or nil if not found
def lookup_default(class_name, param_name)
return nil unless hiera_enabled?
return nil if @common_data.nil?

# Try to lookup with class prefix: modulename::parametername
key = "#{class_name}::#{param_name}"
return nil unless @common_data.key?(key)

value = @common_data[key]

# Convert value to Puppet-compatible string representation
value_to_puppet_string(value)
end

private

# Loads and parses hiera.yaml from the module root
# @return [Hash, nil] The parsed hiera configuration, or nil if not found/invalid
def load_hiera_config
hiera_file = File.join(@module_path, 'hiera.yaml')
load_yaml_data(hiera_file)
end

# Finds the path to the first static hierarchy layer (without interpolations)
# @return [String, nil] The full path to the data file, or nil if not found
def find_first_static_layer_path
return nil unless hiera_enabled?

# Get datadir from hiera config (defaults to 'data')
datadir = @hiera_config.dig('defaults', 'datadir') || 'data'

# Find first hierarchy entry without interpolations
hierarchy = @hiera_config['hierarchy']
return nil unless hierarchy

first_static = hierarchy.find do |entry|
path_or_paths = entry['path'] || entry['paths']
next false unless path_or_paths

# Check if path(s) contain interpolations like %{...}
case path_or_paths
when String
!path_or_paths.include?('%{')
when Array
path_or_paths.none? { |p| p.include?('%{') }
else
false
end
end

return nil unless first_static

# Get the path from the hierarchy entry
data_file_path = first_static['path'] || first_static['paths']&.first
return nil unless data_file_path

# Build and return full path
File.join(@module_path, datadir, data_file_path)
end

# Loads and parses a YAML data file
# @param [String] file_path The full path to the YAML file to load
# @return [Hash, nil] The parsed YAML data, or nil if not found/invalid
def load_yaml_data(file_path)
return nil unless File.exist?(file_path)

begin
YAML.load_file(file_path)
rescue StandardError => e
YARD::Logger.instance.warn "Failed to parse #{File.basename(file_path)}: #{e.message}"
nil
end
end

# Loads and parses the first static hierarchy layer (without interpolations)
# @return [Hash, nil] The parsed data, or nil if not found/invalid
def load_common_data
data_file = find_first_static_layer_path
return nil unless data_file

load_yaml_data(data_file)
end

# Converts a Ruby value to a Puppet-compatible string representation
# @param [Object] value The value to convert
# @return [String] The Puppet-compatible string representation
def value_to_puppet_string(value)
case value
when String
# Empty strings from YAML nil (~) should be undef
return 'undef' if value.empty?

# Strings should be quoted
"'#{value}'"
when Integer, Float, TrueClass, FalseClass
# Numbers and booleans are unquoted/lowercase
value.to_s
when NilClass, :undef
# Puppet undef
'undef'
when Hash
# Convert hash to Puppet hash syntax (no spaces to match code defaults format)
return '{}' if value.empty?

pairs = value.map { |k, v| "'#{k}' => #{value_to_puppet_string(v)}" }
"{ #{pairs.join(', ')} }"
when Array
# Convert array to Puppet array syntax (no spaces to match code defaults format)
return '[]' if value.empty?

elements = value.map { |v| value_to_puppet_string(v) }
"[#{elements.join(', ')}]"
else
# Fallback: convert to string and quote
"'#{value}'"
end
end
end
end
63 changes: 62 additions & 1 deletion lib/openvox-strings/markdown/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require 'openvox-strings/json'
require 'openvox-strings/yard'
require 'openvox-strings/markdown/helpers'
require 'openvox-strings/hiera'

# Implements classes that make elements in a YARD::Registry hash easily accessible for template.
module OpenvoxStrings::Markdown
Expand Down Expand Up @@ -86,6 +87,7 @@ def initialize(registry, component_type)
@type = component_type
@registry = registry
@tags = registry[:docstring][:tags] || []
@hiera = initialize_hiera
end

# generate 1:1 tag methods
Expand Down Expand Up @@ -174,7 +176,13 @@ def enums_for_param(parameter_name)

# @return [Hash] any defaults found for the component
def defaults
@registry[:defaults] unless @registry[:defaults].nil?
# Start with code defaults from the Puppet class
code_defaults = @registry[:defaults] || {}

# Try to merge with Hiera defaults if available
merged_defaults = merge_hiera_defaults(code_defaults)

merged_defaults.empty? ? nil : merged_defaults
end

# @return [Hash] information needed for the table of contents
Expand Down Expand Up @@ -216,6 +224,59 @@ def render(template)

private

# Initializes Hiera integration for this component
# @return [OpenvoxStrings::Hiera, nil] Hiera instance or nil if not available
def initialize_hiera
return nil unless @registry[:file]

# Find the module root directory from the file path
# Puppet modules have manifests/, lib/, data/ etc. at the root
module_path = find_module_root(@registry[:file])
return nil unless module_path

OpenvoxStrings::Hiera.new(module_path)
rescue StandardError => e
YARD::Logger.instance.debug "Failed to initialize Hiera: #{e.message}"
nil
end

# Finds the module root directory from a file path
# @param [String] file_path The path to a file in the module
# @return [String, nil] The module root path or nil if not found
def find_module_root(file_path)
names = %w[metadata.json manifests hiera.yaml]
Pathname.new(file_path).expand_path.parent.ascend do |current_path|
names.each do |name|
return current_path if (current_path / name).exist?
end
end
end

# Merges code defaults with Hiera defaults
# @param [Hash] code_defaults The defaults from the Puppet code
# @return [Hash] Merged defaults with code defaults taking precedence
def merge_hiera_defaults(code_defaults)
return code_defaults unless @hiera&.hiera_enabled?

# Start with Hiera defaults
merged = {}

# Get all parameters from the docstring
param_tags = @tags.select { |tag| tag[:tag_name] == 'param' }

param_tags.each do |param_tag|
param_name = param_tag[:name]
next unless param_name

# Try to get default from Hiera
hiera_default = @hiera.lookup_default(name, param_name)
merged[param_name] = hiera_default if hiera_default
end

# Code defaults override Hiera defaults
merged.merge(code_defaults)
end

def select_tags(name)
tags = @tags.select { |tag| tag[:tag_name] == name }
tags.empty? ? nil : tags
Expand Down
Loading