Refactoring: added sanity checks in setters

This commit is contained in:
mpeterv
2014-03-02 00:49:44 +04:00
parent 73467e2836
commit db3e21e944
3 changed files with 180 additions and 125 deletions

View File

@@ -7,10 +7,10 @@ describe("tests related to actions", function()
local parser = Parser() local parser = Parser()
parser:option "-f" "--from" { parser:option "-f" "--from" {
action = action1 action = function(...) return action1(...) end
} }
parser:option "-p" "--pair" { parser:option "-p" "--pair" {
action = action2, action = function(...) return action2(...) end,
count = "*", count = "*",
args = 2 args = 2
} }
@@ -28,14 +28,14 @@ describe("tests related to actions", function()
local parser = Parser() local parser = Parser()
parser:flag "-v" "--verbose" { parser:flag "-v" "--verbose" {
action = action1, action = function(...) return action1(...) end,
count = "0-3" count = "0-3"
} }
parser:flag "-q" "--quiet" { parser:flag "-q" "--quiet" {
action = action2 action = function(...) return action2(...) end
} }
parser:flag "-a" "--another-flag" { parser:flag "-a" "--another-flag" {
action = action3 action = function(...) return action3(...) end
} }
local args = parser:parse{"-vv", "--quiet"} local args = parser:parse{"-vv", "--quiet"}
@@ -53,7 +53,7 @@ describe("tests related to actions", function()
count = "0-3" count = "0-3"
} }
local add = parser:command "add" { local add = parser:command "add" {
action = action action = function(...) return action(...) end
} }
add:argument "something" add:argument "something"

View File

@@ -20,7 +20,7 @@ describe("tests related to :pparse()", function()
it("still raises an error if it is caused by misconfiguration", function() it("still raises an error if it is caused by misconfiguration", function()
local parser = Parser() local parser = Parser()
parser:option "--foo" parser:option "--foo"
:count "a lot" :aliases {1, 2, 3}
assert.has_error(function() parser:pparse{} end) assert.has_error(function() parser:pparse{} end)
end) end)
end) end)

View File

@@ -1,101 +1,201 @@
local class = require "30log" local class = require "30log"
local Declarative = {} local function parse_boundaries(boundaries)
if tonumber(boundaries) then
return tonumber(boundaries), tonumber(boundaries)
end
function Declarative:__init(...) if boundaries == "*" then
local cl = getmetatable(self) return 0, math.huge
end
for _, field in ipairs(self._fields) do if boundaries == "+" then
if not cl[field] then return 1, math.huge
cl[field] = function(s, value) end
s["_" .. field] = value
return s if boundaries == "?" then
return 0, 1
end
if boundaries:match "^%d+%-%d+$" then
local min, max = boundaries:match "^(%d+)%-(%d+)$"
return tonumber(min), tonumber(max)
end
if boundaries:match "^%d+%+$" then
local min = boundaries:match "^(%d+)%+$"
return tonumber(min), math.huge
end
end
local function add_setters(cl, fields)
for field, setter in pairs(fields) do
cl[field] = function(self, value)
if setter then
setter(self, value)
end end
self["_"..field] = value
return self
end end
end end
self(...) cl.__init = function(self, ...)
end return self(...)
end
function Declarative:__call(...) cl.__call = function(self, ...)
local name_or_options local name_or_options
for i=1, select("#", ...) do for i=1, select("#", ...) do
name_or_options = select(i, ...) name_or_options = select(i, ...)
if type(name_or_options) == "string" then if type(name_or_options) == "string" then
if self._aliases then if self._aliases then
table.insert(self._aliases, name_or_options) table.insert(self._aliases, name_or_options)
end end
if not self._name then if not self._name then
self._name = name_or_options self._name = name_or_options
end end
elseif type(name_or_options) == "table" then elseif type(name_or_options) == "table" then
for _, field in ipairs(self._fields) do for field, setter in pairs(fields) do
if name_or_options[field] ~= nil then if name_or_options[field] ~= nil then
self["_" .. field] = name_or_options[field] self[field](self, name_or_options[field])
end
end end
end end
end end
return self
end end
return self return cl
end end
local Parser = class { local typecheck = setmetatable({}, {
__index = function(self, type_)
local typechecker_factory = function(field)
return function(_, value)
if type(value) ~= type_ then
error(("bad field '%s' (%s expected, got %s)"):format(field, type_, type(value)))
end
end
end
self[type_] = typechecker_factory
return typechecker_factory
end
})
local noop = false
local function aliased_name(self, name)
typecheck.string "name" (self, name)
table.insert(self._aliases, name)
end
local function aliased_aliases(self, aliases)
typecheck.table "aliases" (self, aliases)
if not self._name then
self._name = aliases[1]
end
end
local function boundaries(field)
return function(self, value)
local min, max = parse_boundaries(value)
if not min then
error(("bad field '%s'"):format(field))
end
self["_min"..field], self["_max"..field] = min, max
end
end
local function convert(self, value)
if type(value) ~= "function" then
if type(value) ~= "table" then
error(("bad field 'convert' (function or table expected, got %s)"):format(type(value)))
end
end
end
local Parser = add_setters(class {
__name = "Parser", __name = "Parser",
_arguments = {}, _arguments = {},
_options = {}, _options = {},
_commands = {}, _commands = {},
_require_command = true, _require_command = true,
_add_help = true, _add_help = true
_fields = { }, {
"name", "description", "epilog", "require_command", name = typecheck.string "name",
"usage", "help", "add_help" description = typecheck.string "description",
} epilog = typecheck.string "epilog",
}:include(Declarative) require_command = typecheck.boolean "require_command",
usage = typecheck.string "usage",
help = typecheck.string "help",
add_help = noop
})
local Command = Parser:extends { local Command = add_setters(Parser:extends {
__name = "Command", __name = "Command",
_aliases = {}, _aliases = {}
_fields = { }, {
"name", "aliases", "description", "epilog", name = aliased_name,
"target", "require_command", "action", "usage", aliases = aliased_aliases,
"help", "add_help" description = typecheck.string "description",
} epilog = typecheck.string "epilog",
} target = typecheck.string "target",
require_command = typecheck.boolean "require_command",
action = typecheck["function"] "action",
usage = typecheck.string "usage",
help = typecheck.string "help",
add_help = noop
})
local Argument = class { local Argument = add_setters(class {
__name = "Argument", __name = "Argument",
_args = 1, _minargs = 1,
_count = 1, _maxargs = 1,
_defmode = "unused", _mincount = 1,
_fields = { _maxcount = 1,
"name", "description", "target", "args", _defmode = "unused"
"minargs", "maxargs", "default", "defmode", }, {
"convert", "usage", "argname" name = typecheck.string "name",
} description = typecheck.string "description",
}:include(Declarative) target = typecheck.string "target",
args = boundaries "args",
default = typecheck.string "default",
defmode = typecheck.string "defmode",
convert = convert,
usage = typecheck.string "usage",
argname = typecheck.string "argname"
})
local Option = Argument:extends { local Option = add_setters(Argument:extends {
__name = "Option", __name = "Option",
_aliases = {}, _aliases = {},
_count = "?", _mincount = 0,
_overwrite = true, _overwrite = true
_fields = { }, {
"name", "aliases", "description", "target", name = aliased_name,
"args", "minargs", "maxargs", "count", aliases = aliased_aliases,
"mincount", "maxcount", "default", "defmode", description = typecheck.string "description",
"convert", "overwrite", "action", "usage", target = typecheck.string "target",
"argname" args = boundaries "args",
} count = boundaries "count",
} default = typecheck.string "default",
defmode = typecheck.string "defmode",
local Flag = Option:extends { convert = convert,
__name = "Flag", overwrite = typecheck.boolean "overwrite",
_args = 0 action = typecheck["function"] "action",
} usage = typecheck.string "usage",
argname = typecheck.string "argname"
})
function Argument:get_arg_usage(argname) function Argument:get_arg_usage(argname)
argname = self._argname or argname argname = self._argname or argname
@@ -169,47 +269,8 @@ function Argument:make_type()
end end
end end
local function parse_boundaries(boundaries)
if tonumber(boundaries) then
return tonumber(boundaries), tonumber(boundaries)
end
if boundaries == "*" then
return 0, math.huge
end
if boundaries == "+" then
return 1, math.huge
end
if boundaries == "?" then
return 0, 1
end
if boundaries:match "^%d+%-%d+$" then
local min, max = boundaries:match "^(%d+)%-(%d+)$"
return tonumber(min), tonumber(max)
end
if boundaries:match "^%d+%+$" then
local min = boundaries:match "^(%d+)%+$"
return tonumber(min), math.huge
end
end
function Argument:make_boundaries()
if not self._minargs or not self._maxargs then
self._minargs, self._maxargs = parse_boundaries(self._args)
end
if not self._mincount or not self._maxcount then
self._mincount, self._maxcount = parse_boundaries(self._count)
end
end
function Argument:prepare() function Argument:prepare()
self:make_target() self:make_target()
self:make_boundaries()
self:make_type() self:make_type()
return self return self
end end
@@ -255,7 +316,7 @@ function Parser:option(...)
end end
function Parser:flag(...) function Parser:flag(...)
local flag = Flag:new(...) local flag = Option:new():args(0)(...)
table.insert(self._options, flag) table.insert(self._options, flag)
return flag return flag
end end
@@ -270,22 +331,16 @@ function Parser:prepare()
self._fullname = self._fullname or self._name self._fullname = self._fullname or self._name
if self._add_help and not self._help_option then if self._add_help and not self._help_option then
self._help_option = self:flag(self._add_help) self._help_option = self:flag()
:description "Show this help message and exit. "
:action(function() :action(function()
io.stdout:write(self:get_help() .. "\r\n") io.stdout:write(self:get_help() .. "\r\n")
os.exit(0) os.exit(0)
end) end)
(self._add_help)
if #self._help_option._aliases == 0 then if not self._help_option._name then
if self._help_option._name then self._help_option "-h" "--help"
self._help_option._aliases[1] = self._help_option._name
else
self._help_option "-h" "--help"
end
end
if not self._help_option._description then
self._help_option:description "Show this help message and exit. "
end end
end end