System.Configuration

March 12, 2019 ยท View on GitHub

The configurations are name-type pairs that'd be registered in anywhere with or without special handlers, so we can parse the config settings in one place for the whole system.

The configurations are usually divided into groups, so here is a group of configurations:

Configurations   = {
	Debug        = Boolean,
	LogLevel     = Logger.LogLevel,
	LogHandler   = Callable,
	ErrorHandler = Callable,

	Application  = {
		Root     = String,
		Version  = Number,
	}
}

The config settings are also divided into groups, the preivous configurations would match a group of config settings like :

Config           = {
	Debug        = false,
	LogLevel     = Logger.LogLevel.Info,
	LogHandler   = print,
	ErrorHandler = print,

	Application  = {
		Root     = "/lib",
		Version  = 10010,
	}
}

The configurations are names with require types, and the config settings are names with require type values.

The configurations in a group may be registered from several parts, and the config settings should be distributed to their special handlers.

System.Configuration.ConfigSection

The config sections are used as containers for configurations. The config settings can be parsed by it, then the section'd raise events to distribute the settings.

Start with an example:

require "PLoop" (function(_ENV)
	import "System.Configuration"

	-- Configurations
	section                     = ConfigSection()

	section.Field.Debug         = Boolean
	section.Field.LogLevel      = Logger.LogLevel
	section.Field.LogHandler    = Callable
	section.Field.ErrorHandler  = Callable

	-- Configurations.Application
	local appsec                = section.Section.Application

	appsec.Field.Root           = String
	appsec.Field.Version        = Number

	-- Field handler for section
	function section:OnFieldParse(name, val, ...)
		print("[SECTION]", name, val, ...)
	end

	-- Config handler for section
	function section:OnParse(config, ...)
		print("[SECTION FIN]", config.Debug, config.LogLevel, ...)
	end

	-- Field handler for app section
	function appsec:OnFieldParse(name, val, ...)
		print("[APP]", name, val, ...)
	end

	-- Config handler for app section
	function appsec:OnParse(config, ...)
		print("[APP FIN]", config.Root, config.Version, ...)
	end

	-- Parse the config settings
	section:ParseConfig({
		Debug                   = false,
		LogLevel                = Logger.LogLevel.Info,
		LogHandler              = print,
		ErrorHandler            = print,

		Application             = {
			Root                = "/lib",
			Version             = 10010,
		}
	}, "PARSE CONFIG")
end)

The result should be like

[SECTION]     Debug           false                 PARSE CONFIG
[SECTION]     LogLevel        2                     PARSE CONFIG
[SECTION]     LogHandler      function: 00C77308    PARSE CONFIG
[SECTION]     ErrorHandler    function: 00C77308    PARSE CONFIG
[APP]         Root            /lib                  PARSE CONFIG
[APP]         Version         10010                 PARSE CONFIG
[APP FIN]     /lib            10010                 PARSE CONFIG
[SECTION FIN] false           2                     PARSE CONFIG

Config Section

Each config section is a table contains several name-type paris, so the config value should match the type of the same name. That's the field settings like section.Field.Debug = Boolean.

Each config section can contains other config sections, we can get new or existed section through section.Section.XXXXXX.

We can travese the section through GetFields and GetSections method, they'll return the fields or sections with the order, the first defined would be the first returned:

require "PLoop" (function(_ENV)
	import "System.Configuration"

	section                     = ConfigSection()

	section.Field.Debug         = Boolean
	section.Field.LogLevel      = Logger.LogLevel
	section.Field.LogHandler    = Callable
	section.Field.ErrorHandler  = Callable

	local appsec                = section.Section.Application

	appsec.Field.Root           = String
	appsec.Field.Version        = Number

	-- Print the structure
	function printSection(sec, indent, name)
		indent = indent and (indent .. "    " ) or ""

		if name then
			print(indent .. name .. " = {")
		else
			print(indent .. "{")
		end

		-- Traverse the fields
		for name, type in sec:GetFields() do
			print(indent .. "    " .. name .. " = " .. type .. ",")
		end

		-- Traverse the sections
		for name, subsec in sec:GetSections() do
			printSection(subsec, indent, name)
		end

		if indent == "" then
			print("}")
		else
			print(indent .. "},")
		end
	end

	--- {
	---     Debug = System.Boolean,
	---     LogLevel = System.Logger.LogLevel,
	---     LogHandler = System.Callable,
	---     ErrorHandler = System.Callable,
	---     Application = {
	---         Root = System.String,
	---         Version = System.Number,
	---     },
	--- }
	printSection(section)
end)

Parse Config Settings

The ParseConfig 's arguments is defined like

__Arguments__{ Table, Any * 0 }
function ParseConfig(self, config, ...)
end

It'd receive a config table and any other arguments would be passed through the events. In the previous example, the "PARSE CONFIG" is passed to the event handlers.

In the ParseConfig, name-value pairs would be distributed by OnFieldParse attribute, and the parsed config would be distributed by OnParse at last.

__ConfigSection__

Using event handler is not a self-document code style, and since the configurations are defined all over the places, we need a clear and self-document code style to declare the configurations.

The Configurations system provided an attribute __ConfigSection__ for functions.

Let's have an example:

require "PLoop" (function(_ENV)
	import "System.Configuration"

	class "HtmlRender" (function(_ENV)
		__Static__() property "ConfigSection" { set = false, default = Configuration.ConfigSection() }

		__Static__() property "Config" {
			type      = Table,
			throwable = true,
			set       = function(self, config)
				if config then
					local ret, msg = HtmlRender.ConfigSection:ParseConfig(config)
					if msg then throw(Struct.GetErrorMessage(msg, "HtmlRender.Config")) end
				end
			end,
		}

		__Static__() property "NoLineBreak" { type = Boolean }
		__Static__() property "NoIndent"    { type = Boolean }
		__Static__() property "DebugMode"   { type = Boolean }
	end)

	-------------------------------------
	-- Register config section or field setting
	-------------------------------------
	__ConfigSection__( HtmlRender.ConfigSection.Render, { nolinebreak = Boolean, noindent = Boolean  } )
	function HtmlRenderConfig(config, ...)
		print("CONFIG", config.nolinebreak, config.noindent)
		HtmlRender.NoLineBreak = config.nolinebreak
		HtmlRender.NoIndent    = config.noindent
	end

	__ConfigSection__( HtmlRender.ConfigSection, "debug", Boolean)
	function HtmlRenderDebug(field, value, ...)
		print("DEBUG", field, value)
		HtmlRender.DebugMode = value
	end

	-------------------------------------
	-- Usage
	-------------------------------------
	-- CONFIG	false	true
	-- DEBUG	debug	true
	HtmlRender.Config   = {
		Render = {
			nolinebreak = false,
			noindent    = true,
		},
		debug  = true,
	}

	-- false	true	true
	print(HtmlRender.NoLineBreak, HtmlRender.NoIndent, HtmlRender.DebugMode)
end)

So we only decalre the config section and where to parse the config in the class, register the config section or config in any other places.

Then we ccan provide the config in any config file and let the config handlers do the real jobs.

BTW, the HtmlRender.ConfigSection.Render is a shortcut for HtmlRender.ConfigSection.Section.Render, through the config section's __index meta-method, it can access its fields or sections, since the Render is not existed in the config section, it'll be created as a sub config section automatically.