configset.lua 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. --
  2. -- base/configset.lua
  3. --
  4. -- A configuration set manages a collection of fields, which are organized
  5. -- into "blocks". Each block stores a set of field-value pairs, along with
  6. -- a list of terms which indicate the context in which those field values
  7. -- should be applied.
  8. --
  9. -- Configurations use the field definitions to know what fields are available,
  10. -- and the corresponding value types for those fields. Only fields that have
  11. -- been registered via field.new() can be stored.
  12. --
  13. -- TODO: I may roll this functionality up into the container API at some
  14. -- point. If you find yourself using or extending this code for your own
  15. -- work give me a shout before you go too far with it so we can coordinate.
  16. --
  17. -- Copyright (c) 2012-2014 Jason Perkins and the Premake project
  18. --
  19. local p = premake
  20. p.configset = {}
  21. local configset = p.configset
  22. local criteria = p.criteria
  23. --
  24. -- Create a new configuration set.
  25. --
  26. -- @param parent
  27. -- An optional parent configuration set. If provided, the parent provides
  28. -- a base configuration, which this set will extend.
  29. -- @return
  30. -- A new, empty configuration set.
  31. --
  32. function configset.new(parent)
  33. local cset = {}
  34. cset.parent = parent
  35. cset.blocks = {}
  36. cset.current = nil
  37. cset.compiled = false
  38. return cset
  39. end
  40. ---
  41. -- Retrieve a value from the configuration set.
  42. --
  43. -- This and the criteria supporting code are the inner loops of the app. Some
  44. -- readability has been sacrificed for overall performance.
  45. --
  46. -- @param cset
  47. -- The configuration set to query.
  48. -- @param field
  49. -- The definition of field to be queried.
  50. -- @param filter
  51. -- A list of lowercase context terms to use during the fetch. Only those
  52. -- blocks with terms fully contained by this list will be considered in
  53. -- determining the returned value. Terms should be lower case to make
  54. -- the context filtering case-insensitive.
  55. -- @param ctx
  56. -- The context that will be used for detoken.expand
  57. -- @param origin
  58. -- The originating configset if set.
  59. -- @return
  60. -- The requested value.
  61. ---
  62. function configset.fetch(cset, field, filter, ctx, origin)
  63. filter = filter or {}
  64. ctx = ctx or {}
  65. if p.field.merges(field) then
  66. return configset._fetchMerged(cset, field, filter, ctx, origin)
  67. else
  68. return configset._fetchDirect(cset, field, filter, ctx, origin)
  69. end
  70. end
  71. function configset._dofilter(cset, block, filter)
  72. if not filter.matcher then
  73. return (cset.compiled or criteria.matches(block._criteria, filter))
  74. else
  75. return filter.matcher(cset, block, filter)
  76. end
  77. end
  78. function configset._fetchDirect(cset, field, filter, ctx, origin)
  79. -- If the originating configset hasn't been compiled, then the value will still
  80. -- be on that configset.
  81. if origin and origin ~= cset and not origin.compiled then
  82. return configset._fetchDirect(origin, field, filter, ctx, origin)
  83. end
  84. local abspath = filter.files
  85. local basedir
  86. local key = field.name
  87. local blocks = cset.blocks
  88. local n = #blocks
  89. for i = n, 1, -1 do
  90. local block = blocks[i]
  91. if not origin or block._origin == origin then
  92. local value = block[key]
  93. -- If the filter contains a file path, make it relative to
  94. -- this block's basedir
  95. if value ~= nil and abspath and not cset.compiled and block._basedir and block._basedir ~= basedir then
  96. basedir = block._basedir
  97. filter.files = path.getrelative(basedir, abspath)
  98. end
  99. if value ~= nil and configset._dofilter(cset, block, filter) then
  100. -- If value is an object, return a copy of it so that any
  101. -- changes later made to it by the caller won't alter the
  102. -- original value (that was a tough bug to find)
  103. if type(value) == "table" then
  104. value = table.deepcopy(value)
  105. end
  106. -- Detoken
  107. if field.tokens and ctx.environ then
  108. value = p.detoken.expand(value, ctx.environ, field, ctx._basedir)
  109. end
  110. return value
  111. end
  112. end
  113. end
  114. filter.files = abspath
  115. if cset.parent then
  116. return configset._fetchDirect(cset.parent, field, filter, ctx, origin)
  117. end
  118. end
  119. function configset._fetchMerged(cset, field, filter, ctx, origin)
  120. -- If the originating configset hasn't been compiled, then the value will still
  121. -- be on that configset.
  122. if origin and origin ~= cset and not origin.compiled then
  123. return configset._fetchMerged(origin, field, filter, ctx, origin)
  124. end
  125. local result = {}
  126. local function remove(patterns)
  127. for _, pattern in ipairs(patterns) do
  128. -- Detoken
  129. if field.tokens and ctx.environ then
  130. pattern = p.detoken.expand(pattern, ctx.environ, field, ctx._basedir)
  131. end
  132. pattern = path.wildcards(pattern):lower()
  133. local j = 1
  134. while j <= #result do
  135. local value = result[j]:lower()
  136. if value:match(pattern) == value then
  137. result[result[j]] = nil
  138. table.remove(result, j)
  139. else
  140. j = j + 1
  141. end
  142. end
  143. end
  144. end
  145. if cset.parent then
  146. result = configset._fetchMerged(cset.parent, field, filter, ctx, origin)
  147. end
  148. local abspath = filter.files
  149. local basedir
  150. local key = field.name
  151. local blocks = cset.blocks
  152. local n = #blocks
  153. for i = 1, n do
  154. local block = blocks[i]
  155. if not origin or block._origin == origin then
  156. -- If the filter contains a file path, make it relative to
  157. -- this block's basedir
  158. if abspath and block._basedir and block._basedir ~= basedir and not cset.compiled then
  159. basedir = block._basedir
  160. filter.files = path.getrelative(basedir, abspath)
  161. end
  162. if configset._dofilter(cset, block, filter) then
  163. if block._removes and block._removes[key] then
  164. remove(block._removes[key])
  165. end
  166. local value = block[key]
  167. -- If value is an object, return a copy of it so that any
  168. -- changes later made to it by the caller won't alter the
  169. -- original value (that was a tough bug to find)
  170. if type(value) == "table" then
  171. value = table.deepcopy(value)
  172. end
  173. if value then
  174. -- Detoken
  175. if field.tokens and ctx.environ then
  176. value = p.detoken.expand(value, ctx.environ, field, ctx._basedir)
  177. end
  178. -- Translate
  179. if field and p.field.translates(field) then
  180. value = p.field.translate(field, value)
  181. end
  182. result = p.field.merge(field, result, value)
  183. end
  184. end
  185. end
  186. end
  187. filter.files = abspath
  188. return result
  189. end
  190. ---
  191. -- Create and return a metatable which allows a configuration set to act as a
  192. -- "backing store" for a regular Lua table. Table operations that access a
  193. -- registered field will fetch from or store to the configurations set, while
  194. -- unknown keys are get and set to the table normally.
  195. ---
  196. function configset.metatable(cset)
  197. return {
  198. __newindex = function(tbl, key, value)
  199. local f = p.field.get(key)
  200. if f then
  201. local status, err = configset.store(cset, f, value)
  202. if err then
  203. error(err, 2)
  204. end
  205. else
  206. rawset(tbl, key, value)
  207. return value
  208. end
  209. end,
  210. __index = function(tbl, key)
  211. local f = p.field.get(key)
  212. if f then
  213. return configset.fetch(cset, f)
  214. else
  215. return nil
  216. end
  217. end
  218. }
  219. end
  220. ---
  221. -- Create a new block of configuration field-value pairs, using a set of
  222. -- old-style, non-prefixed context terms to control their application. This
  223. -- approach will eventually be phased out in favor of prefixed filters;
  224. -- see addFilter() below.
  225. --
  226. -- @param cset
  227. -- The configuration set to hold the new block.
  228. -- @param terms
  229. -- A set of context terms to control the application of values contained
  230. -- in the block.
  231. -- @param basedir
  232. -- An optional base directory; if set, filename filter tests will be made
  233. -- relative to this basis before pattern testing.
  234. -- @return
  235. -- The new configuration data block.
  236. ---
  237. function configset.addblock(cset, terms, basedir)
  238. configset.addFilter(cset, terms, basedir, true)
  239. return cset.current
  240. end
  241. ---
  242. -- Create a new block of configuration field-value pairs, using a set
  243. -- of new-style, prefixed context terms to control their application.
  244. --
  245. -- @param cset
  246. -- The configuration set to hold the new block.
  247. -- @param terms
  248. -- A set of terms used to control the application of the values
  249. -- contained in the block.
  250. -- @param basedir
  251. -- An optional base directory. If set, filename filter tests will be
  252. -- made relative to this base before pattern testing.
  253. -- @param unprefixed
  254. -- If true, uses the old, unprefixed style for filter terms. This will
  255. -- eventually be phased out in favor of prefixed filters.
  256. ---
  257. function configset.addFilter(cset, terms, basedir, unprefixed)
  258. local crit, err = criteria.new(terms, unprefixed)
  259. if not crit then
  260. return nil, err
  261. end
  262. local block = {}
  263. block._criteria = crit
  264. block._origin = cset
  265. if basedir then
  266. block._basedir = basedir:lower()
  267. end
  268. table.insert(cset.blocks, block)
  269. cset.current = block
  270. return true
  271. end
  272. ---
  273. -- Allow calling code to save and restore a filter. Particularly useful for
  274. -- modules.
  275. ---
  276. function configset.getFilter(cset)
  277. return {
  278. _criteria = cset.current._criteria,
  279. _basedir = cset.current._basedir
  280. }
  281. end
  282. function configset.setFilter(cset, filter)
  283. local block = {}
  284. block._criteria = filter._criteria
  285. block._basedir = filter._basedir
  286. block._origin = cset
  287. table.insert(cset.blocks, block)
  288. cset.current = block;
  289. end
  290. ---
  291. -- Add a new field-value pair to the current configuration data block. The
  292. -- data type of the field is taken into account when adding the values:
  293. -- strings are replaced, arrays are merged, etc.
  294. --
  295. -- @param cset
  296. -- The configuration set to hold the new value.
  297. -- @param fieldname
  298. -- The name of the field being set. The field should have already been
  299. -- defined using the api.register() function.
  300. -- @param value
  301. -- The new value for the field.
  302. -- @return
  303. -- If successful, returns true. If an error occurred, returns nil and
  304. -- an error message.
  305. ---
  306. function configset.store(cset, field, value)
  307. if not cset.current then
  308. configset.addblock(cset, {})
  309. end
  310. local key = field.name
  311. local current = cset.current
  312. local status, result = pcall(function ()
  313. current[key] = p.field.store(field, current[key], value)
  314. end)
  315. if not status then
  316. if type(result) == "table" then
  317. result = result.msg
  318. end
  319. return nil, result
  320. end
  321. return true
  322. end
  323. --
  324. -- Remove values from a configuration set.
  325. --
  326. -- @param cset
  327. -- The configuration set from which to remove.
  328. -- @param field
  329. -- The field holding the values to be removed.
  330. -- @param values
  331. -- A list of values to be removed.
  332. --
  333. function configset.remove(cset, field, values)
  334. -- removes are always processed first; starting a new block here
  335. -- ensures that they will be processed in the proper order
  336. local block = {}
  337. block._basedir = cset.current._basedir
  338. block._criteria = cset.current._criteria
  339. block._origin = cset
  340. table.insert(cset.blocks, block)
  341. cset.current = block
  342. -- TODO This comment is not completely valid anymore
  343. -- This needs work; right now it is hardcoded to only work for lists.
  344. -- To support removing from keyed collections, I first need to figure
  345. -- out how to move the wildcard():lower() bit into the value
  346. -- processing call chain (i.e. that should happen somewhere inside of
  347. -- the field.remove() call). And then I will probably need to add
  348. -- another accessor to actually do the removing, which right now is
  349. -- hardcoded inside of _fetchMerged(). Oh, and some of the logic in
  350. -- api.remove() needs to get pushed down to here (or field).
  351. values = p.field.remove(field, {}, values)
  352. -- add a list of removed values to the block
  353. current = cset.current
  354. current._removes = {}
  355. current._removes[field.name] = values
  356. end
  357. --
  358. -- Check to see if a configuration set is empty; that is, it does
  359. -- not contain any configuration blocks.
  360. --
  361. -- @param cset
  362. -- The configuration set to query.
  363. -- @return
  364. -- True if the set does not contain any blocks.
  365. --
  366. function configset.empty(cset)
  367. return (#cset.blocks == 0)
  368. end
  369. --
  370. -- Compiles a new configuration set containing only the blocks which match
  371. -- the specified criteria. Fetches against this compiled configuration set
  372. -- may omit the context argument, resulting in faster fetches against a
  373. -- smaller set of configuration blocks.
  374. --
  375. -- @param cset
  376. -- The configuration set to query.
  377. -- @param filter
  378. -- A list of lowercase context terms to use during the fetch. Only those
  379. -- blocks with terms fully contained by this list will be considered in
  380. -- determining the returned value. Terms should be lower case to make
  381. -- the context filtering case-insensitive.
  382. -- @return
  383. -- A new configuration set containing only the selected blocks, and the
  384. -- "compiled" field set to true.
  385. --
  386. function configset.compile(cset, filter)
  387. -- always start with the parent
  388. local result
  389. if cset.parent then
  390. result = configset.compile(cset.parent, filter)
  391. else
  392. result = configset.new()
  393. end
  394. local blocks = cset.blocks
  395. local n = #blocks
  396. local abspath = filter.files
  397. local basedir
  398. for i = 1, n do
  399. local block = blocks[i]
  400. if block._origin == cset then
  401. block._origin = result
  402. end
  403. -- If the filter contains a file path, make it relative to
  404. -- this block's basedir
  405. if abspath and block._basedir and block._basedir ~= basedir then
  406. basedir = block._basedir
  407. filter.files = path.getrelative(basedir, abspath)
  408. end
  409. if criteria.matches(block._criteria, filter) then
  410. table.insert(result.blocks, block)
  411. end
  412. end
  413. filter.files = abspath
  414. result.compiled = true
  415. return result
  416. end