Skip to content
112 changes: 112 additions & 0 deletions lib/openvox-strings/hiera.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# 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')
return nil unless File.exist?(hiera_file)

begin
YAML.load_file(hiera_file)
rescue StandardError => e
YARD::Logger.instance.warn "Failed to parse hiera.yaml: #{e.message}"
nil
end
Comment thread
slauger marked this conversation as resolved.
Outdated
end

# Loads and parses data/common.yaml from the module
# @return [Hash, nil] The parsed common data, or nil if not found/invalid
def load_common_data
return nil unless hiera_enabled?

# Get datadir from hiera config (defaults to 'data')
datadir = @hiera_config.dig('defaults', 'datadir') || 'data'
common_file = File.join(@module_path, datadir, 'common.yaml')

return nil unless File.exist?(common_file)

begin
YAML.load_file(common_file)
rescue StandardError => e
YARD::Logger.instance.warn "Failed to parse common.yaml: #{e.message}"
nil
end
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
74 changes: 73 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,70 @@ 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)
current_path = File.dirname(File.expand_path(file_path))

# Walk up the directory tree looking for module indicators
10.times do
# Check if this looks like a module root (has manifests/, lib/, or hiera.yaml)
if File.exist?(File.join(current_path, 'hiera.yaml')) ||
(File.exist?(File.join(current_path, 'manifests')) && File.exist?(File.join(current_path, 'metadata.json')))
return current_path
end

parent = File.dirname(current_path)
break if parent == current_path # Reached filesystem root

current_path = parent
end

nil
Comment thread
slauger marked this conversation as resolved.
Outdated
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