#!/usr/bin/lua
--[[

UCI Validation Layer - Command Line Utility
(c) 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
(c) 2008 Steven Barth <steven@midlink.org>

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

        http://www.apache.org/licenses/LICENSE-2.0

$Id$

]]--

require("luci.uvl")
require("luci.util")


function getopt( arg, options )
	options = options or ""
	local tab = {}
	local args = {}
	for k, v in ipairs(arg) do
		if v:sub(1, 2) == "--" then
			local x = v:find( "=", 1, true )
			if x then
				tab[ v:sub( 3, x-1 ) ] = v:sub( x+1 )
			else
			    tab[ v:sub( 3 ) ] = true
			end
		elseif v:sub( 1, 1 ) == "-" then
			local y = 2
			local l = #v
			local jopt
			while ( y <= l ) do
				jopt = v:sub( y, y )
				if options:find( jopt, 1, true ) then
					if y < l then
						tab[ jopt ] = v:sub( y+1 )
						y = l
					else
						tab[ jopt ] = arg[ k + 1 ]
						arg[ k + 1 ] = ""
					end
				else
					tab[ jopt ] = true
				end
				y = y + 1
			end
	    elseif #v > 0 then
	    	table.insert(args, v)
	    end
	end
	return tab, args
end

function genspec(conf)
	require("luci.model.uci")
	require("luci.uvl.datatypes")

	local uci     = luci.model.uci.cursor()
	local ok, err = uci:load(conf)

	if not ok then
		print("Can not load config:", err)
		os.exit(1)
	else
		local function _guess_datatype(v)
			if type(v) == "table" then v = v[1] end

			for _, type in ipairs({
				"boolean", "integer", "float", "ip4addr", "ip6addr",
				"macaddr", "directory", "file"
			}) do
				if luci.uvl.datatypes[type](v) then
					return type
				end
			end
			return "string"
		end


		local co = uci:get_all(conf)
		local ct = { }
		local ca = { }
		local so = { }
		local to = { }

		-- count section types
		for _, section in pairs(co) do
			ct[section['.type']] = ( ct[section['.type']] or 0 ) + 1
			ca[section['.type']] = section['.anonymous']
			so[section['.type']] = so[section['.type']] or { }
			to[section['.type']] = to[section['.type']] or { }

			for option, value in pairs(section) do
				if option:sub(1,1) ~= "." then
					so[section['.type']][option] = _guess_datatype(value)
					to[section['.type']][option] = ( type(value) == "table" and "list" or "variable" )
				end
			end
		end

		-- package name
		print( "package %s" % conf )

		-- write section schemes
		for type, count in luci.util.kspairs(ct) do
			print( "\nconfig section" )
			print( "\toption name	'%s'" % type )
			print( "\toption title	'Section %s'" % type )
			print( "\toption package	'%s'"% conf )
			print( "\toption named	%s" % ( ca[type] and 'false' or 'true' ) )
			print( "\toption unique	%s" % ( ct[type] > 1 and 'false' or ( ca[type] and 'false' or 'true' ) ) )
			print( "\toption dynamic	false" )
			print( "\toption required	false" )

			-- write option schemes
			for opt, val in luci.util.kspairs(so[type]) do
				print( "\nconfig variable" )
				print( "\toption name	'%s'" % opt )
				print( "\toption title	'Option %s'" % opt )
				print( "\toption section	'%s.%s'" %{ conf, type } )
				print( "\toption datatype	'%s'" % so[type][opt] )

				if to[type][opt] ~= "variable" then
					print( "\toption type	'%s'" % to[type][opt] )
				end
			end

			print("")
		end

	end
end


local options, arguments = getopt( arg )

if #arguments ~= 2 or options.help then
	print([=[

uvl - UCI Validation Layer
$Id$
(c) 2008 Jo-Philipp Wich, Steven Barth

Usage:
	uvl --help
	uvl [--silent] [--schemedir=DIR] [--configdir=DIR] [--no-strict-sections] \
		[--no-strict-options] [--no-strict-validators] [--no-strict-lists] \
		{verify|verify-scheme|genspec} config[.section[.option]]

Options:
	--help
		Display this help message.

	--silent
		Don't produce any output.

	--schemedir=DIR
		Use DIR as scheme directory.

	--configdir=DIR
		Use DIR as config directory.

	--no-strict-sections
		Don't treat sections found in config but not in scheme as error.

	--no-strict-options
		Don't treat options found in config but not in scheme as error.

	--no-strict-validators
		Don't invalidate config if an external validator fails.

	--no-strict-lists
		Don't invalidate lists that are stored options.

Actions:
	verify
		Validate given configuration, section or option.

	verify-scheme
		Validate given scheme against the reference meta scheme.

	genspec
		Generate a scheme skeleton from given configuration.
	]=])
	os.exit(255)
elseif arguments[1] == "verify" or arguments[1] == "verify-scheme" then
	luci.uvl.STRICT_UNKNOWN_SECTIONS =
		( not options['no-strict-sections'] and true or false )
	luci.uvl.STRICT_UNKNOWN_OPTIONS =
		( not options['no-strict-options'] and true or false )
	luci.uvl.STRICT_EXTERNAL_VALIDATORS =
		( not options['no-strict-validators'] and true or false )
	luci.uvl.STRICT_LIST_TYPE =
		( not options['no-strict-lists'] and true or false )

	local uvl = luci.uvl.UVL(
		type(options.schemedir) == "string" and options.schemedir
	)

	local cso = luci.util.split( arguments[2], "." )
	local ok, err

	if arguments[1] == "verify-scheme" then
		uvl:read_scheme( 'schema', cso[1] )

		local uci = uvl.uci.cursor(
			type(options.configdir) == "string"
				and options.configdir or uvl.schemedir .. '/default'
		)

		ok, err = uvl:validate_config( cso[1], uci:get_all(cso[1]) )
		if err then err.code = luci.uvl.errors.ERR_SCHEME end
	else
		ok, err = uvl:validate( unpack(cso) )
	end

	if ok then
		if not options.silent then
			print( string.format(
				'%s "%s" validates fine!',
					( arguments[1] == "verify-scheme" and "Scheme" or
						( #cso == 1 and "Config" or
							( #cso == 2 and "Section" or "Option" ) ) ),
					table.concat(cso, ".")
			) )
		end
		os.exit( 0 )
	else
		if not options.silent then print( err and err:string() or "Unknown error" ) end
		os.exit( 1 )
	end
else
	genspec( arguments[2] )
end
