diff options
author | Hisham Muhammad <hisham@gobolinux.org> | 2014-09-09 23:05:59 -0300 |
---|---|---|
committer | Hisham Muhammad <hisham@gobolinux.org> | 2014-09-09 23:05:59 -0300 |
commit | f0d66aeacd60e6df926a55ecb9a08e6f802f9242 (patch) | |
tree | 9f8a83fe0756a4d7103e1fe9513f04887d05cfd4 | |
parent | 0587afbb5fe8ceb2f2eea16f486bd6183bf02f29 (diff) | |
download | luarocks-f0d66aeacd60e6df926a55ecb9a08e6f802f9242.tar.gz luarocks-f0d66aeacd60e6df926a55ecb9a08e6f802f9242.tar.bz2 luarocks-f0d66aeacd60e6df926a55ecb9a08e6f802f9242.zip |
Support per-field version checking.
This will allow us to add fields and bump rockspec version numbers
in a well-behaved manner.
-rw-r--r-- | src/luarocks/type_check.lua | 274 |
1 files changed, 158 insertions, 116 deletions
diff --git a/src/luarocks/type_check.lua b/src/luarocks/type_check.lua index a78c4848..4adb7cf4 100644 --- a/src/luarocks/type_check.lua +++ b/src/luarocks/type_check.lua | |||
@@ -6,72 +6,95 @@ local type_check = {} | |||
6 | package.loaded["luarocks.type_check"] = type_check | 6 | package.loaded["luarocks.type_check"] = type_check |
7 | 7 | ||
8 | local cfg = require("luarocks.cfg") | 8 | local cfg = require("luarocks.cfg") |
9 | local deps = require("luarocks.deps") | ||
9 | 10 | ||
10 | type_check.rockspec_format = "1.0" | 11 | type_check.rockspec_format = "1.1" |
12 | |||
13 | local string_1 = { _type = "string" } | ||
14 | local mandatory_string_1 = { _type = "string", _mandatory = true } | ||
15 | |||
16 | -- Syntax for type-checking tables: | ||
17 | -- | ||
18 | -- A type-checking table describes typing data for a value. | ||
19 | -- Any key starting with an underscore has a special meaning: | ||
20 | -- _type (string) is the Lua type of the value. Default is "table". | ||
21 | -- _version (string) is the minimum rockspec_version that supports this value. Default is "1.0". | ||
22 | -- _mandatory (boolean) indicates if the value is a mandatory key in its container table. Default is false. | ||
23 | -- For "string" types only: | ||
24 | -- _pattern (string) is the string-matching pattern, valid for string types only. Default is ".*". | ||
25 | -- For "table" types only: | ||
26 | -- _any (table) is the type-checking table for unspecified keys, recursively checked. | ||
27 | -- _more (boolean) indicates that the table accepts unspecified keys and does not type-check them. | ||
28 | -- Any other string keys that don't start with an underscore represent known keys and are type-checking tables, recursively checked. | ||
11 | 29 | ||
12 | local rockspec_types = { | 30 | local rockspec_types = { |
13 | rockspec_format = "string", | 31 | rockspec_format = string_1, |
14 | MUST_package = "string", | 32 | package = mandatory_string_1, |
15 | MUST_version = "[%w.]+-[%d]+", | 33 | version = { _type = "string", _pattern = "[%w.]+-[%d]+", _mandatory = true }, |
16 | description = { | 34 | description = { |
17 | summary = "string", | 35 | summary = string_1, |
18 | detailed = "string", | 36 | detailed = string_1, |
19 | homepage = "string", | 37 | homepage = string_1, |
20 | license = "string", | 38 | license = string_1, |
21 | maintainer = "string" | 39 | maintainer = string_1, |
22 | }, | 40 | }, |
23 | dependencies = { | 41 | dependencies = { |
24 | platforms = {}, | 42 | platforms = {}, -- recursively defined below |
25 | ANY = "string" | 43 | _any = string_1, |
26 | }, | 44 | }, |
27 | supported_platforms = { | 45 | supported_platforms = { |
28 | ANY = "string" | 46 | _any = string_1, |
29 | }, | 47 | }, |
30 | external_dependencies = { | 48 | external_dependencies = { |
31 | platforms = {}, | 49 | platforms = {}, -- recursively defined below |
32 | ANY = { | 50 | _any = { |
33 | program = "string", | 51 | program = string_1, |
34 | header = "string", | 52 | header = string_1, |
35 | library = "string" | 53 | library = string_1, |
36 | } | 54 | } |
37 | }, | 55 | }, |
38 | MUST_source = { | 56 | source = { |
39 | platforms = {}, | 57 | _mandatory = true, |
40 | MUST_url = "string", | 58 | platforms = {}, -- recursively defined below |
41 | md5 = "string", | 59 | url = mandatory_string_1, |
42 | file = "string", | 60 | md5 = string_1, |
43 | dir = "string", | 61 | file = string_1, |
44 | tag = "string", | 62 | dir = string_1, |
45 | branch = "string", | 63 | tag = string_1, |
46 | module = "string", | 64 | branch = string_1, |
47 | cvs_tag = "string", | 65 | module = string_1, |
48 | cvs_module = "string" | 66 | cvs_tag = string_1, |
67 | cvs_module = string_1, | ||
49 | }, | 68 | }, |
50 | build = { | 69 | build = { |
51 | platforms = {}, | 70 | platforms = {}, -- recursively defined below |
52 | type = "string", | 71 | type = string_1, |
53 | install = { | 72 | install = { |
54 | lua = { | 73 | lua = { |
55 | MORE = true | 74 | _more = true |
56 | }, | 75 | }, |
57 | lib = { | 76 | lib = { |
58 | MORE = true | 77 | _more = true |
59 | }, | 78 | }, |
60 | conf = { | 79 | conf = { |
61 | MORE = true | 80 | _more = true |
62 | }, | 81 | }, |
63 | bin = { | 82 | bin = { |
64 | MORE = true | 83 | _more = true |
65 | } | 84 | } |
66 | }, | 85 | }, |
67 | copy_directories = { | 86 | copy_directories = { |
68 | ANY = "string" | 87 | _any = string_1, |
69 | }, | 88 | }, |
70 | MORE = true | 89 | _more = true |
71 | }, | 90 | }, |
72 | hooks = { | 91 | hooks = { |
73 | platforms = {}, | 92 | platforms = {}, -- recursively defined below |
74 | post_install = "string" | 93 | post_install = string_1, |
94 | }, | ||
95 | deploy = { | ||
96 | _version = "1.1", | ||
97 | wrap_bin_scripts = { _type = "boolean", _version = "1.1" }, | ||
75 | } | 98 | } |
76 | } | 99 | } |
77 | 100 | ||
@@ -82,69 +105,61 @@ type_check.rockspec_order = {"rockspec_format", "package", "version", | |||
82 | { "build", {"type", "modules", "copy_directories", "platforms"} }, | 105 | { "build", {"type", "modules", "copy_directories", "platforms"} }, |
83 | "hooks"} | 106 | "hooks"} |
84 | 107 | ||
85 | function type_check.load_extensions() | 108 | rockspec_types.build.platforms._any = rockspec_types.build |
86 | type_check.rockspec_format = "1.1" | 109 | rockspec_types.dependencies.platforms._any = rockspec_types.dependencies |
87 | rockspec_types.deploy = { | 110 | rockspec_types.external_dependencies.platforms._any = rockspec_types.external_dependencies |
88 | wrap_bin_scripts = true, | 111 | rockspec_types.source.platforms._any = rockspec_types.source |
89 | } | 112 | rockspec_types.hooks.platforms._any = rockspec_types.hooks |
90 | end | ||
91 | |||
92 | if cfg.use_extensions then | ||
93 | type_check.load_extensions() | ||
94 | end | ||
95 | |||
96 | rockspec_types.build.platforms.ANY = rockspec_types.build | ||
97 | rockspec_types.dependencies.platforms.ANY = rockspec_types.dependencies | ||
98 | rockspec_types.external_dependencies.platforms.ANY = rockspec_types.external_dependencies | ||
99 | rockspec_types.MUST_source.platforms.ANY = rockspec_types.MUST_source | ||
100 | rockspec_types.hooks.platforms.ANY = rockspec_types.hooks | ||
101 | 113 | ||
102 | local manifest_types = { | 114 | local manifest_types = { |
103 | MUST_repository = { | 115 | repository = { |
116 | _mandatory = true, | ||
104 | -- packages | 117 | -- packages |
105 | ANY = { | 118 | _any = { |
106 | -- versions | 119 | -- versions |
107 | ANY = { | 120 | _any = { |
108 | -- items | 121 | -- items |
109 | ANY = { | 122 | _any = { |
110 | MUST_arch = "string", | 123 | arch = mandatory_string_1, |
111 | modules = { ANY = "string" }, | 124 | modules = { _any = string_1 }, |
112 | commands = { ANY = "string" }, | 125 | commands = { _any = string_1 }, |
113 | dependencies = { ANY = "string" }, | 126 | dependencies = { _any = string_1 }, |
114 | -- TODO: to be extended with more metadata. | 127 | -- TODO: to be extended with more metadata. |
115 | } | 128 | } |
116 | } | 129 | } |
117 | } | 130 | } |
118 | }, | 131 | }, |
119 | MUST_modules = { | 132 | modules = { |
133 | _mandatory = true, | ||
120 | -- modules | 134 | -- modules |
121 | ANY = { | 135 | _any = { |
122 | -- providers | 136 | -- providers |
123 | ANY = "string" | 137 | _any = string_1 |
124 | } | 138 | } |
125 | }, | 139 | }, |
126 | MUST_commands = { | 140 | commands = { |
141 | _mandatory = true, | ||
127 | -- modules | 142 | -- modules |
128 | ANY = { | 143 | _any = { |
129 | -- commands | 144 | -- commands |
130 | ANY = "string" | 145 | _any = string_1 |
131 | } | 146 | } |
132 | }, | 147 | }, |
133 | dependencies = { | 148 | dependencies = { |
134 | -- each module | 149 | -- each module |
135 | ANY = { | 150 | _any = { |
136 | -- each version | 151 | -- each version |
137 | ANY = { | 152 | _any = { |
138 | -- each dependency | 153 | -- each dependency |
139 | ANY = { | 154 | _any = { |
140 | name = "string", | 155 | name = string_1, |
141 | constraints = { | 156 | constraints = { |
142 | ANY = { | 157 | _any = { |
143 | no_upgrade = "boolean", | 158 | no_upgrade = { _type = "boolean" }, |
144 | op = "string", | 159 | op = string_1, |
145 | version = { | 160 | version = { |
146 | string = "string", | 161 | string = string_1, |
147 | ANY = 0, | 162 | _any = 0, |
148 | } | 163 | } |
149 | } | 164 | } |
150 | } | 165 | } |
@@ -154,54 +169,75 @@ local manifest_types = { | |||
154 | } | 169 | } |
155 | } | 170 | } |
156 | 171 | ||
172 | local function check_version(version, typetbl, context) | ||
173 | local typetbl_version = typetbl._version or "1.0" | ||
174 | if deps.compare_versions(typetbl_version, version) then | ||
175 | if context == "" then | ||
176 | return nil, "Invalid rockspec_format version number in rockspec? Please fix rockspec accordingly." | ||
177 | else | ||
178 | return nil, context.." is not supported in rockspec format "..version.." (requires version "..typetbl_version.."), please fix the rockspec_format field accordingly." | ||
179 | end | ||
180 | end | ||
181 | return true | ||
182 | end | ||
183 | |||
157 | local type_check_table | 184 | local type_check_table |
158 | 185 | ||
159 | --- Type check an object. | 186 | --- Type check an object. |
160 | -- The object is compared against an archetypical value | 187 | -- The object is compared against an archetypical value |
161 | -- matching the expected type -- the actual values don't matter, | 188 | -- matching the expected type -- the actual values don't matter, |
162 | -- only their types. Tables are type checked recursively. | 189 | -- only their types. Tables are type checked recursively. |
163 | -- @param name any: The object name (for error messages). | 190 | -- @param version string: The version of the item. |
164 | -- @param item any: The object being checked. | 191 | -- @param item any: The object being checked. |
165 | -- @param expected any: The reference object. In case of a table, | 192 | -- @param typetbl any: The type-checking table for the object. |
166 | -- its is structured as a type reference table. | ||
167 | -- @param context string: A string indicating the "context" where the | 193 | -- @param context string: A string indicating the "context" where the |
168 | -- error occurred (such as the name of the table the item is a part of), | 194 | -- error occurred (the full table path), for error messages. |
169 | -- to be used by error messages. | ||
170 | -- @return boolean or (nil, string): true if type checking | 195 | -- @return boolean or (nil, string): true if type checking |
171 | -- succeeded, or nil and an error message if it failed. | 196 | -- succeeded, or nil and an error message if it failed. |
172 | -- @see type_check_table | 197 | -- @see type_check_table |
173 | local function type_check_item(name, item, expected, context) | 198 | local function type_check_item(version, item, typetbl, context) |
174 | name = tostring(name) | 199 | assert(type(version) == "string") |
175 | 200 | ||
176 | local item_type = type(item) | 201 | local ok, err = check_version(version, typetbl, context) |
177 | local expected_type = type(expected) | 202 | if not ok then |
203 | return nil, err | ||
204 | end | ||
205 | |||
206 | local item_type = type(item) or "nil" | ||
207 | local expected_type = typetbl._type or "table" | ||
208 | |||
178 | if expected_type == "number" then | 209 | if expected_type == "number" then |
179 | if not tonumber(item) then | 210 | if not tonumber(item) then |
180 | return nil, "Type mismatch on field "..context..name..": expected a number" | 211 | return nil, "Type mismatch on field "..context..": expected a number" |
181 | end | 212 | end |
182 | elseif expected_type == "string" then | 213 | elseif expected_type == "string" then |
183 | if type(item) ~= "string" then | 214 | if item_type ~= "string" then |
184 | return nil, "Type mismatch on field "..context..name..": expected a string" | 215 | return nil, "Type mismatch on field "..context..": expected a string, got "..item_type |
185 | end | 216 | end |
186 | if expected ~= "string" then | 217 | if typetbl._pattern then |
187 | if item_type ~= "string" then | 218 | if not item:match("^"..typetbl._pattern.."$") then |
188 | return nil, "Type mismatch on field "..context..name..": expected a string, got a "..type(item) | 219 | return nil, "Type mismatch on field "..context..": invalid value "..item.." does not match '"..expected.."'" |
189 | elseif not item:match("^"..expected.."$") then | ||
190 | return nil, "Type mismatch on field "..context..name..": invalid value "..item.." does not match '"..expected.."'" | ||
191 | end | 220 | end |
192 | end | 221 | end |
193 | elseif expected_type == "table" then | 222 | elseif expected_type == "table" then |
194 | if item_type ~= expected_type then | 223 | if item_type ~= expected_type then |
195 | return nil, "Type mismatch on field "..context..name..": expected a table" | 224 | return nil, "Type mismatch on field "..context..": expected a table" |
196 | else | 225 | else |
197 | return type_check_table(item, expected, context..name..".") | 226 | return type_check_table(version, item, typetbl, context) |
198 | end | 227 | end |
199 | elseif item_type ~= expected_type then | 228 | elseif item_type ~= expected_type then |
200 | return nil, "Type mismatch on field "..context..name..": expected a "..expected_type | 229 | return nil, "Type mismatch on field "..context..": expected "..expected_type |
201 | end | 230 | end |
202 | return true | 231 | return true |
203 | end | 232 | end |
204 | 233 | ||
234 | local function mkfield(context, field) | ||
235 | if context == "" then | ||
236 | return field | ||
237 | end | ||
238 | return context.."."..field | ||
239 | end | ||
240 | |||
205 | --- Type check the contents of a table. | 241 | --- Type check the contents of a table. |
206 | -- The table's contents are compared against a reference table, | 242 | -- The table's contents are compared against a reference table, |
207 | -- which contains the recognized fields, with archetypical values | 243 | -- which contains the recognized fields, with archetypical values |
@@ -215,23 +251,31 @@ end | |||
215 | -- with MUST_, it is mandatory; its absence from the table is | 251 | -- with MUST_, it is mandatory; its absence from the table is |
216 | -- a type error. | 252 | -- a type error. |
217 | -- Tables are type checked recursively. | 253 | -- Tables are type checked recursively. |
254 | -- @param version string: The version of tbl. | ||
218 | -- @param tbl table: The table to be type checked. | 255 | -- @param tbl table: The table to be type checked. |
219 | -- @param types table: The reference table, containing | 256 | -- @param typetbl table: The type-checking table, containing |
220 | -- values for recognized fields in the checked table. | 257 | -- values for recognized fields in the checked table. |
221 | -- @param context string: A string indicating the "context" where the | 258 | -- @param context string: A string indicating the "context" where the |
222 | -- error occurred (such as the name of the table the item is a part of), | 259 | -- error occurred (such as the name of the table the item is a part of), |
223 | -- to be used by error messages. | 260 | -- to be used by error messages. |
224 | -- @return boolean or (nil, string): true if type checking | 261 | -- @return boolean or (nil, string): true if type checking |
225 | -- succeeded, or nil and an error message if it failed. | 262 | -- succeeded, or nil and an error message if it failed. |
226 | type_check_table = function(tbl, types, context) | 263 | type_check_table = function(version, tbl, typetbl, context) |
264 | assert(type(version) == "string") | ||
227 | assert(type(tbl) == "table") | 265 | assert(type(tbl) == "table") |
228 | assert(type(types) == "table") | 266 | assert(type(typetbl) == "table") |
267 | |||
268 | local ok, err = check_version(version, typetbl, context) | ||
269 | if not ok then | ||
270 | return nil, err | ||
271 | end | ||
272 | |||
229 | for k, v in pairs(tbl) do | 273 | for k, v in pairs(tbl) do |
230 | local t = types[k] or (type(k) == "string" and types["MUST_"..k]) or types.ANY | 274 | local t = typetbl[k] or typetbl._any |
231 | if t then | 275 | if t then |
232 | local ok, err = type_check_item(k, v, t, context) | 276 | local ok, err = type_check_item(version, v, t, mkfield(context, k)) |
233 | if not ok then return nil, err end | 277 | if not ok then return nil, err end |
234 | elseif types.MORE then | 278 | elseif typetbl._more then |
235 | -- Accept unknown field | 279 | -- Accept unknown field |
236 | else | 280 | else |
237 | if not cfg.accept_unknown_fields then | 281 | if not cfg.accept_unknown_fields then |
@@ -239,21 +283,20 @@ type_check_table = function(tbl, types, context) | |||
239 | end | 283 | end |
240 | end | 284 | end |
241 | end | 285 | end |
242 | for k, v in pairs(types) do | 286 | for k, v in pairs(typetbl) do |
243 | local mandatory_key = k:match("^MUST_(.+)") | 287 | if k:sub(1,1) ~= "_" and v._mandatory then |
244 | if mandatory_key then | 288 | if not tbl[k] then |
245 | if not tbl[mandatory_key] then | 289 | return nil, "Mandatory field "..mkfield(context, k).." is missing." |
246 | return nil, "Mandatory field "..context..mandatory_key.." is missing." | ||
247 | end | 290 | end |
248 | end | 291 | end |
249 | end | 292 | end |
250 | return true | 293 | return true |
251 | end | 294 | end |
252 | 295 | ||
253 | local function check_undeclared_globals(globals, types) | 296 | local function check_undeclared_globals(globals, typetbl) |
254 | local undeclared = {} | 297 | local undeclared = {} |
255 | for glob, _ in pairs(globals) do | 298 | for glob, _ in pairs(globals) do |
256 | if not (types[glob] or types["MUST_"..glob]) then | 299 | if not (typetbl[glob] or typetbl["MUST_"..glob]) then |
257 | table.insert(undeclared, glob) | 300 | table.insert(undeclared, glob) |
258 | end | 301 | end |
259 | end | 302 | end |
@@ -273,13 +316,12 @@ end | |||
273 | -- succeeded, or nil and an error message if it failed. | 316 | -- succeeded, or nil and an error message if it failed. |
274 | function type_check.type_check_rockspec(rockspec, globals) | 317 | function type_check.type_check_rockspec(rockspec, globals) |
275 | assert(type(rockspec) == "table") | 318 | assert(type(rockspec) == "table") |
276 | if rockspec.rockspec_format then | 319 | if not rockspec.rockspec_format then |
277 | -- relies on global state | 320 | rockspec.rockspec_format = "1.0" |
278 | type_check.load_extensions() | ||
279 | end | 321 | end |
280 | local ok, err = check_undeclared_globals(globals, rockspec_types) | 322 | local ok, err = check_undeclared_globals(globals, rockspec_types) |
281 | if not ok then return nil, err end | 323 | if not ok then return nil, err end |
282 | return type_check_table(rockspec, rockspec_types, "") | 324 | return type_check_table(rockspec.rockspec_format, rockspec, rockspec_types, "") |
283 | end | 325 | end |
284 | 326 | ||
285 | --- Type check a manifest table. | 327 | --- Type check a manifest table. |
@@ -292,7 +334,7 @@ function type_check.type_check_manifest(manifest, globals) | |||
292 | assert(type(manifest) == "table") | 334 | assert(type(manifest) == "table") |
293 | local ok, err = check_undeclared_globals(globals, manifest_types) | 335 | local ok, err = check_undeclared_globals(globals, manifest_types) |
294 | if not ok then return nil, err end | 336 | if not ok then return nil, err end |
295 | return type_check_table(manifest, manifest_types, "") | 337 | return type_check_table("1.0", manifest, manifest_types, "") |
296 | end | 338 | end |
297 | 339 | ||
298 | return type_check | 340 | return type_check |