123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499 |
- --
- -- base/configset.lua
- --
- -- A configuration set manages a collection of fields, which are organized
- -- into "blocks". Each block stores a set of field-value pairs, along with
- -- a list of terms which indicate the context in which those field values
- -- should be applied.
- --
- -- Configurations use the field definitions to know what fields are available,
- -- and the corresponding value types for those fields. Only fields that have
- -- been registered via field.new() can be stored.
- --
- -- TODO: I may roll this functionality up into the container API at some
- -- point. If you find yourself using or extending this code for your own
- -- work give me a shout before you go too far with it so we can coordinate.
- --
- -- Copyright (c) 2012-2014 Jason Perkins and the Premake project
- --
- local p = premake
- p.configset = {}
- local configset = p.configset
- local criteria = p.criteria
- --
- -- Create a new configuration set.
- --
- -- @param parent
- -- An optional parent configuration set. If provided, the parent provides
- -- a base configuration, which this set will extend.
- -- @return
- -- A new, empty configuration set.
- --
- function configset.new(parent)
- local cset = {}
- cset.parent = parent
- cset.blocks = {}
- cset.current = nil
- cset.compiled = false
- return cset
- end
- ---
- -- Retrieve a value from the configuration set.
- --
- -- This and the criteria supporting code are the inner loops of the app. Some
- -- readability has been sacrificed for overall performance.
- --
- -- @param cset
- -- The configuration set to query.
- -- @param field
- -- The definition of field to be queried.
- -- @param filter
- -- A list of lowercase context terms to use during the fetch. Only those
- -- blocks with terms fully contained by this list will be considered in
- -- determining the returned value. Terms should be lower case to make
- -- the context filtering case-insensitive.
- -- @param ctx
- -- The context that will be used for detoken.expand
- -- @param origin
- -- The originating configset if set.
- -- @return
- -- The requested value.
- ---
- function configset.fetch(cset, field, filter, ctx, origin)
- filter = filter or {}
- ctx = ctx or {}
- if p.field.merges(field) then
- return configset._fetchMerged(cset, field, filter, ctx, origin)
- else
- return configset._fetchDirect(cset, field, filter, ctx, origin)
- end
- end
- function configset._dofilter(cset, block, filter)
- if not filter.matcher then
- return (cset.compiled or criteria.matches(block._criteria, filter))
- else
- return filter.matcher(cset, block, filter)
- end
- end
- function configset._fetchDirect(cset, field, filter, ctx, origin)
- -- If the originating configset hasn't been compiled, then the value will still
- -- be on that configset.
- if origin and origin ~= cset and not origin.compiled then
- return configset._fetchDirect(origin, field, filter, ctx, origin)
- end
- local abspath = filter.files
- local basedir
- local key = field.name
- local blocks = cset.blocks
- local n = #blocks
- for i = n, 1, -1 do
- local block = blocks[i]
- if not origin or block._origin == origin then
- local value = block[key]
- -- If the filter contains a file path, make it relative to
- -- this block's basedir
- if value ~= nil and abspath and not cset.compiled and block._basedir and block._basedir ~= basedir then
- basedir = block._basedir
- filter.files = path.getrelative(basedir, abspath)
- end
- if value ~= nil and configset._dofilter(cset, block, filter) then
- -- If value is an object, return a copy of it so that any
- -- changes later made to it by the caller won't alter the
- -- original value (that was a tough bug to find)
- if type(value) == "table" then
- value = table.deepcopy(value)
- end
- -- Detoken
- if field.tokens and ctx.environ then
- value = p.detoken.expand(value, ctx.environ, field, ctx._basedir)
- end
- return value
- end
- end
- end
- filter.files = abspath
- if cset.parent then
- return configset._fetchDirect(cset.parent, field, filter, ctx, origin)
- end
- end
- function configset._fetchMerged(cset, field, filter, ctx, origin)
- -- If the originating configset hasn't been compiled, then the value will still
- -- be on that configset.
- if origin and origin ~= cset and not origin.compiled then
- return configset._fetchMerged(origin, field, filter, ctx, origin)
- end
- local result = {}
- local function remove(patterns)
- for _, pattern in ipairs(patterns) do
- -- Detoken
- if field.tokens and ctx.environ then
- pattern = p.detoken.expand(pattern, ctx.environ, field, ctx._basedir)
- end
- pattern = path.wildcards(pattern):lower()
- local j = 1
- while j <= #result do
- local value = result[j]:lower()
- if value:match(pattern) == value then
- result[result[j]] = nil
- table.remove(result, j)
- else
- j = j + 1
- end
- end
- end
- end
- if cset.parent then
- result = configset._fetchMerged(cset.parent, field, filter, ctx, origin)
- end
- local abspath = filter.files
- local basedir
- local key = field.name
- local blocks = cset.blocks
- local n = #blocks
- for i = 1, n do
- local block = blocks[i]
- if not origin or block._origin == origin then
- -- If the filter contains a file path, make it relative to
- -- this block's basedir
- if abspath and block._basedir and block._basedir ~= basedir and not cset.compiled then
- basedir = block._basedir
- filter.files = path.getrelative(basedir, abspath)
- end
- if configset._dofilter(cset, block, filter) then
- if block._removes and block._removes[key] then
- remove(block._removes[key])
- end
- local value = block[key]
- -- If value is an object, return a copy of it so that any
- -- changes later made to it by the caller won't alter the
- -- original value (that was a tough bug to find)
- if type(value) == "table" then
- value = table.deepcopy(value)
- end
- if value then
- -- Detoken
- if field.tokens and ctx.environ then
- value = p.detoken.expand(value, ctx.environ, field, ctx._basedir)
- end
- -- Translate
- if field and p.field.translates(field) then
- value = p.field.translate(field, value)
- end
- result = p.field.merge(field, result, value)
- end
- end
- end
- end
- filter.files = abspath
- return result
- end
- ---
- -- Create and return a metatable which allows a configuration set to act as a
- -- "backing store" for a regular Lua table. Table operations that access a
- -- registered field will fetch from or store to the configurations set, while
- -- unknown keys are get and set to the table normally.
- ---
- function configset.metatable(cset)
- return {
- __newindex = function(tbl, key, value)
- local f = p.field.get(key)
- if f then
- local status, err = configset.store(cset, f, value)
- if err then
- error(err, 2)
- end
- else
- rawset(tbl, key, value)
- return value
- end
- end,
- __index = function(tbl, key)
- local f = p.field.get(key)
- if f then
- return configset.fetch(cset, f)
- else
- return nil
- end
- end
- }
- end
- ---
- -- Create a new block of configuration field-value pairs, using a set of
- -- old-style, non-prefixed context terms to control their application. This
- -- approach will eventually be phased out in favor of prefixed filters;
- -- see addFilter() below.
- --
- -- @param cset
- -- The configuration set to hold the new block.
- -- @param terms
- -- A set of context terms to control the application of values contained
- -- in the block.
- -- @param basedir
- -- An optional base directory; if set, filename filter tests will be made
- -- relative to this basis before pattern testing.
- -- @return
- -- The new configuration data block.
- ---
- function configset.addblock(cset, terms, basedir)
- configset.addFilter(cset, terms, basedir, true)
- return cset.current
- end
- ---
- -- Create a new block of configuration field-value pairs, using a set
- -- of new-style, prefixed context terms to control their application.
- --
- -- @param cset
- -- The configuration set to hold the new block.
- -- @param terms
- -- A set of terms used to control the application of the values
- -- contained in the block.
- -- @param basedir
- -- An optional base directory. If set, filename filter tests will be
- -- made relative to this base before pattern testing.
- -- @param unprefixed
- -- If true, uses the old, unprefixed style for filter terms. This will
- -- eventually be phased out in favor of prefixed filters.
- ---
- function configset.addFilter(cset, terms, basedir, unprefixed)
- local crit, err = criteria.new(terms, unprefixed)
- if not crit then
- return nil, err
- end
- local block = {}
- block._criteria = crit
- block._origin = cset
- if basedir then
- block._basedir = basedir:lower()
- end
- table.insert(cset.blocks, block)
- cset.current = block
- return true
- end
- ---
- -- Allow calling code to save and restore a filter. Particularly useful for
- -- modules.
- ---
- function configset.getFilter(cset)
- return {
- _criteria = cset.current._criteria,
- _basedir = cset.current._basedir
- }
- end
- function configset.setFilter(cset, filter)
- local block = {}
- block._criteria = filter._criteria
- block._basedir = filter._basedir
- block._origin = cset
- table.insert(cset.blocks, block)
- cset.current = block;
- end
- ---
- -- Add a new field-value pair to the current configuration data block. The
- -- data type of the field is taken into account when adding the values:
- -- strings are replaced, arrays are merged, etc.
- --
- -- @param cset
- -- The configuration set to hold the new value.
- -- @param fieldname
- -- The name of the field being set. The field should have already been
- -- defined using the api.register() function.
- -- @param value
- -- The new value for the field.
- -- @return
- -- If successful, returns true. If an error occurred, returns nil and
- -- an error message.
- ---
- function configset.store(cset, field, value)
- if not cset.current then
- configset.addblock(cset, {})
- end
- local key = field.name
- local current = cset.current
- local status, result = pcall(function ()
- current[key] = p.field.store(field, current[key], value)
- end)
- if not status then
- if type(result) == "table" then
- result = result.msg
- end
- return nil, result
- end
- return true
- end
- --
- -- Remove values from a configuration set.
- --
- -- @param cset
- -- The configuration set from which to remove.
- -- @param field
- -- The field holding the values to be removed.
- -- @param values
- -- A list of values to be removed.
- --
- function configset.remove(cset, field, values)
- -- removes are always processed first; starting a new block here
- -- ensures that they will be processed in the proper order
- local block = {}
- block._basedir = cset.current._basedir
- block._criteria = cset.current._criteria
- block._origin = cset
- table.insert(cset.blocks, block)
- cset.current = block
- -- TODO This comment is not completely valid anymore
- -- This needs work; right now it is hardcoded to only work for lists.
- -- To support removing from keyed collections, I first need to figure
- -- out how to move the wildcard():lower() bit into the value
- -- processing call chain (i.e. that should happen somewhere inside of
- -- the field.remove() call). And then I will probably need to add
- -- another accessor to actually do the removing, which right now is
- -- hardcoded inside of _fetchMerged(). Oh, and some of the logic in
- -- api.remove() needs to get pushed down to here (or field).
- values = p.field.remove(field, {}, values)
- -- add a list of removed values to the block
- current = cset.current
- current._removes = {}
- current._removes[field.name] = values
- end
- --
- -- Check to see if a configuration set is empty; that is, it does
- -- not contain any configuration blocks.
- --
- -- @param cset
- -- The configuration set to query.
- -- @return
- -- True if the set does not contain any blocks.
- --
- function configset.empty(cset)
- return (#cset.blocks == 0)
- end
- --
- -- Compiles a new configuration set containing only the blocks which match
- -- the specified criteria. Fetches against this compiled configuration set
- -- may omit the context argument, resulting in faster fetches against a
- -- smaller set of configuration blocks.
- --
- -- @param cset
- -- The configuration set to query.
- -- @param filter
- -- A list of lowercase context terms to use during the fetch. Only those
- -- blocks with terms fully contained by this list will be considered in
- -- determining the returned value. Terms should be lower case to make
- -- the context filtering case-insensitive.
- -- @return
- -- A new configuration set containing only the selected blocks, and the
- -- "compiled" field set to true.
- --
- function configset.compile(cset, filter)
- -- always start with the parent
- local result
- if cset.parent then
- result = configset.compile(cset.parent, filter)
- else
- result = configset.new()
- end
- local blocks = cset.blocks
- local n = #blocks
- local abspath = filter.files
- local basedir
- for i = 1, n do
- local block = blocks[i]
- if block._origin == cset then
- block._origin = result
- end
- -- If the filter contains a file path, make it relative to
- -- this block's basedir
- if abspath and block._basedir and block._basedir ~= basedir then
- basedir = block._basedir
- filter.files = path.getrelative(basedir, abspath)
- end
- if criteria.matches(block._criteria, filter) then
- table.insert(result.blocks, block)
- end
- end
- filter.files = abspath
- result.compiled = true
- return result
- end
|