lib/fuguta.rb
# -*- coding: utf-8 -*-
module Fuguta
class ValidationError < StandardError
attr_reader :errors
def initialize(errors)
super("validation error")
@errors = errors
end
end
class SyntaxError < StandardError
# self.cause() is as of Ruby 2.1 so we
# handles root error .
attr_reader :root_cause, :source
def initialize(root_cause, source="")
super("Syntax Error")
raise ArgumentError, 'root_cause' unless root_cause.is_a?(::Exception)
@root_cause = root_cause
@source = source
end
def message
if @root_cause.backtrace.first =~ /:(\d+):in `/ ||
@root_cause.backtrace.first =~ /:(\d+)$/
line = $1.to_i
end
"%s from %s:%d" % [super(), @source, line]
end
end
class Configuration
ValidationError = Fuguta::ValidationError
def self.usual_paths(paths = nil)
if paths
@usual_paths = paths
else
@usual_paths
end
end
def usual_paths
self.class.usual_paths
end
def self.walk_tree(conf, &blk)
raise ArgumentError, "conf must be a 'Configuration'. Got '#{conf.class}'." unless conf.is_a?(Configuration)
blk.call(conf)
conf.config.values.each { |c|
case c
when Configuration
walk_tree(c, &blk)
when Hash
c.values.each { |c1| walk_tree(c1, &blk) if c1.is_a?(Configuration) }
when Array
c.each { |c1| walk_tree(c1, &blk) if c1.is_a?(Configuration) }
end
}
end
class Loader
def initialize(conf)
@conf = conf
end
def load(path = nil)
buf = case path
when NilClass
raise "No path given and usual_paths not set" unless @conf.usual_paths
path = @conf.usual_paths.find { |path| File.exists?(path) } ||
raise("None of the usual paths existed: #{@conf.usual_paths.join(", ")}")
File.read(path)
when String
raise "does not exist: #{path}" unless File.exists?(path)
File.read(path)
when IO
path.lines.join
else
raise "Unknown type: #{path.class}"
end
@conf.parse_dsl do |me|
me.instance_variable_set(:@loading_path, path.to_s)
me.instance_eval(buf, path.to_s)
me.instance_variable_set(:@loading_path, nil)
end
end
def validate
errors = []
Configuration.walk_tree(@conf) do |c|
c.validate(errors)
end
raise ValidationError, errors if errors.size > 0
end
end
class DSLProxy
def initialize(subject)
@subject = subject
@config = subject.config
@loading_path = nil
end
def config
self
end
# Load separate configuration files from the file.
#
# Load relative path file.
# ----------------
# load 'test2.conf'
#
# Load absolute path file.
# ----------------
# load '/etc/test2.conf'
def load(*paths)
l = Loader.new(@subject)
paths.each { |path|
if path =~ %r{^/}
# Load absolute path
l.load(path)
else
# Load relative path
base_conf_dir = (@loading_path.nil? ? Dir.pwd : File.dirname(@loading_path))
l.load(File.expand_path(path, base_conf_dir))
end
}
self
end
end
module ConfigurationMethods
module ClassMethods
# Helper method to define class specific configuration class.
#
# def_configuration(&blk) is available when you include this module
#
# # Example:
# class Base
# include Fuguta::Configuration::ConfigurationMethods
# def_configuration do
# param :xxxx
# param :yyyy
# end
# end
#
# Above example does exactly same thing as below:
#
# class Base
# class Configuration < Fuguta::Configuration
# param :xxxx
# param :yyyy
# end
# @configuration_class = Configuration
# end
#
# # Examples for new classes of Base inheritance.
# class A < Base
# def_configuration do
# param :zzzz
# end
# def_configuration do
# param :xyxy
# end
#
# p Configuration # => A::Configuration
# p Configuration.superclass # => Base::Configuration
# p @configuration_class # => A::Configuration
# end
#
# class B < A
# p self.configuration_class # => A::Configuration
# end
def def_configuration(&blk)
# create new configuration class if not exist.
if self.const_defined?(:Configuration, false)
unless self.const_get(:Configuration, false) < Fuguta::Configuration
raise TypeError, "#{self}::Configuration constant is defined already for another purpose."
end
else
self.const_set(:Configuration, Class.new(self.configuration_class || Fuguta::Configuration))
@configuration_class = self.const_get(:Configuration, false)
end
if blk
@configuration_class.module_eval(&blk)
end
end
def configuration_class
ConfigurationMethods.find_configuration_class(self)
end
end
def self.find_configuration_class(c)
begin
v = c.instance_variable_get(:@configuration_class)
return v if v
if c.const_defined?(:Configuration, false)
return c.const_get(:Configuration, false)
end
end while c = c.superclass
nil
end
private
def self.included(klass)
klass.extend ClassMethods
end
end
class << self
def on_initialize_hook(&blk)
@on_initialize_hooks << blk
end
def on_initialize_hooks
@on_initialize_hooks
end
DEPRECATED_WARNING_MESSAGE="WARN: Deprecated parameter: %1$s. Please use '%2$s'".freeze
DEPRECATED_ERROR_MESSAGE="ERROR: Parameter is no longer supported: %1$s. Please use '%2$s'".freeze
# Show warning message if the old parameter is set.
def deprecated_warn_param(old_name, message=nil)
on_param_create_hook do |param_name, opts|
self.deprecated_warn_for(old_name, param_name, message)
end
end
def deprecated_warn_for(old_name, param_name, message=nil)
warn_msg = message || DEPRECATED_WARNING_MESSAGE
alias_param old_name, param_name
self.const_get(:DSL, false).class_eval %Q{
def #{old_name}(v)
STDERR.puts("#{warn_msg}" % ["#{old_name}", "#{param_name}"])
#{param_name.to_s}(v)
end
}
end
# Raise an error if the old parameter is set.
def deprecated_error_param(old_name, message=nil)
on_param_create_hook do |param_name, opts|
self.deprecated_error_for(old_name, param_name, message)
end
end
def deprecated_error_for(old_name, param_name, message=nil)
err_msg = message || DEPRECATED_ERROR_MESSAGE
alias_param old_name, param_name
self.const_get(:DSL, false).class_eval %Q{
def #{old_name}(v)
raise ("#{err_msg}" % ["#{old_name}", "#{param_name}"])
end
}
end
def alias_param (alias_name, ref_name)
# getter
self.class_eval %Q{
# Ruby alias show error if the method to be defined later is
# set. So create method to call the reference method.
def #{alias_name.to_s}()
#{ref_name}()
end
}
# DSL setter
self.const_get(:DSL, false).class_eval %Q{
def #{alias_name}(v)
#{ref_name.to_s}(v)
end
alias_method :#{alias_name.to_s}=, :#{alias_name.to_s}
}
end
def param(name, opts={})
opts = opts.merge(@opts)
case opts[:default]
when Proc
# create getter method if proc is set as default value
self.class_exec {
define_method(name.to_s.to_sym) do
@config[name.to_s.to_sym] || self.instance_exec(&opts[:default])
end
}
else
on_initialize_hook do |c|
@config[name.to_s.to_sym] = opts[:default]
end
end
@on_param_create_hooks.each { |blk|
blk.call(name.to_s.to_sym, opts)
}
self.const_get(:DSL, false).class_eval %Q{
def #{name}(v)
@config["#{name.to_s}".to_sym] = v
end
alias_method :#{name.to_s}=, :#{name.to_s}
}
@opts.clear
@on_param_create_hooks.clear
end
# Load configuration file
#
# 1. Simply loads single configuration file.
# conf = ConfigurationClass.load('1.conf')
#
# 2. Loads multiple files and merge.
#
# file1.conf:
# config.param1 = 1
#
# file2.conf:
# config.param1 = 2
#
# conf = ConfigurationClass.load('file1.conf', 'file2.conf')
# conf.param1 == 2
def load(*paths)
c = self.new
l = Loader.new(c)
if paths.empty?
l.load
else
paths.each { |path| l.load(path) }
end
l.validate
c
end
# Helper method defines "module DSL" under the current conf class.
#
# This does mostly same things as "module DSL" but using
# "module" statement get the "DSL" constant defined in unexpected
# location if you combind to use with other Ruby DSL syntax.
#
# Usage:
# class Conf1 < Configuration
# DSL do
# end
# end
def DSL(&blk)
self.const_get(:DSL, false).class_eval(&blk)
self
end
private
def inherited(klass)
super
klass.const_set(:DSL, Module.new)
klass.instance_eval {
@on_initialize_hooks = []
@opts = {}
@on_param_create_hooks = []
}
dsl_mods = []
c = klass
while c < Configuration && c.superclass.const_defined?(:DSL, false)
parent_dsl = c.superclass.const_get(:DSL, false)
if parent_dsl && parent_dsl.class === Module
dsl_mods << parent_dsl
end
c = c.superclass
end
# including order is ancestor -> descendants
dsl_mods.reverse.each { |i|
klass.const_get(:DSL, false).__send__(:include, i)
}
end
def on_param_create_hook(&blk)
@on_param_create_hooks << blk
end
end
attr_reader :config, :parent
def initialize(parent=nil)
unless parent.nil?
raise ArgumentError, "parent must be a 'Fuguta::Configuration'. Got '#{parent.class}'." unless parent.is_a?(Fuguta::Configuration)
end
@config = {}
@parent = parent
hook_lst = []
c = self.class
while c < Configuration
hook_lst << c.instance_variable_get(:@on_initialize_hooks)
c = c.superclass
end
hook_lst.reverse.each { |l|
l.each { |c|
self.instance_eval(&c)
}
}
after_initialize
end
def after_initialize
end
def validate(errors)
end
SYNTAX_ERROR_SOURCES=[ScriptError, NameError].freeze
def parse_dsl(&blk)
dsl = self.class.const_get(:DSL, false)
raise "DSL module was not found" unless dsl && dsl.is_a?(Module)
cp_class = DSLProxy.dup
cp_class.__send__(:include, dsl)
cp = cp_class.new(self)
begin
cp.instance_eval(&blk)
rescue *SYNTAX_ERROR_SOURCES => e
raise Fuguta::SyntaxError.new(e, cp.instance_variable_get(:@loading_path))
end
self
end
private
def method_missing(m, *args)
if @config.has_key?(m.to_sym)
@config[m.to_sym]
elsif @config.has_key?(m.to_s)
@config[m.to_s]
else
super
end
end
end
end