--- -- base/field.lua -- -- Fields hold a particular bit of information about a configuration, such -- as the language of a project or the list of files it uses. Each field has -- a particular data "kind", which describes the structure of the information -- it holds, such a simple string, or a list of paths. -- -- The field.* functions here manage the definition of these fields, and the -- accessor functions required to get, set, remove, and merge their values. -- -- Copyright (c) 2014 Jason Perkins and the Premake project --- local p = premake p.field = {} local field = p.field -- Lists to hold all of the registered fields and data kinds field._list = {} field._loweredList = {} field._sortedList = nil field._kinds = {} -- For historical reasons premake.fields = field._list -- A cache for data kind accessor functions field._accessors = {} --- -- Register a new field. -- -- @param f -- A table describing the new field, with these keys: -- name A unique string name for the field, to be used to identify -- the field in future operations. -- kind The kind of values that can be stored into this field. Kinds -- can be chained together to create more complex types, such as -- "list:string". -- -- In addition, any custom keys set on the field description will be -- maintained. -- -- @return -- A populated field object. Or nil and an error message if the field could -- not be registered. --- function field.new(f) -- Translate the old approaches to data kind definitions to the new -- one used here. These should probably be deprecated eventually. if f.kind:startswith("key-") then f.kind = f.kind:sub(5) f.keyed = true end if f.kind:endswith("-list") then f.kind = f.kind:sub(1, -6) f.list = true end local kind = f.kind if kind == "object" or kind == "array" then kind = "table" end if f.list then kind = "list:" .. kind end if f.keyed then kind = "keyed:" .. kind end -- Store the translated kind with a new name, so legacy add-on code -- can continue to work with the old value. f._kind = kind -- Make sure scope is always an array; don't overwrite old value if type(f.scope) == "table" then f.scopes = f.scope else f.scopes = { f.scope } end -- All fields must have a valid store() function if not field.accessor(f, "store") then return nil, "invalid field kind '" .. f._kind .. "'" end field._list[f.name] = f field._loweredList[f.name:lower()] = f field._sortedList = nil return f end --- -- Remove a previously created field definition. --- function field.unregister(f) field._list[f.name] = nil field._loweredList[f.name:lower()] = nil field._sortedList = nil end --- -- Returns an iterator for the list of registered fields; the -- ordering of returned results is arbitrary. --- function field.each() local index return function () index = next(field._list, index) return field._list[index] end end --- -- Returns an iterator for the list of registered fields; the -- results are in a prioritized order, then alphabetized. --- function field.eachOrdered() if not field._sortedList then -- no priorities yet, just alpha sort local keys = table.keys(field._list) table.sort(keys) field._sortedList = {} for i = 1, #keys do field._sortedList[i] = field._list[keys[i]] end end local i = 0 return function () i = i + 1 return field._sortedList[i] end end --- -- Register a new kind of data for field storage. -- -- @param tag -- A unique name of the kind; used in the kind string in new field -- definitions (see new(), above). -- @param settings -- A table containing the processor functions for the new kind. If -- nil, no change is made to the current field settings. -- @return -- The settings table for the specified tag. --- function field.kind(tag, settings) if settings then field._kinds[tag] = settings end return field._kinds[tag] end --- -- Build an "accessor" function to process incoming values for a field. This -- function should be an interview question. -- -- An accessor function takes the form of: -- -- function (field, current, value, nextAccessor) -- -- It receives the target field, the current value of that field, and the new -- value that has been provided by the project script. It then returns the -- new value for the target field. -- -- @param f -- The field for which an accessor should be returned. -- @param method -- The type of accessor function required; currently this should be one of -- "store", "remove", or "merge" though it is possible for add-on modules to -- extend the available methods by implementing appropriate processing -- functions. -- @return -- An accessor function for the field's kind and method. May return nil -- if no processing functions are available for the given method. --- function field.accessor(f, method) -- Prepare a cache for accessors using this method; each encountered -- kind only needs to be fully processed once. field._accessors[method] = field._accessors[method] or {} local cache = field._accessors[method] -- Helper function recurses over each piece of the field's data kind, -- building an accessor function for each sequence encountered. Results -- cached from earlier calls are reused again. local function accessorForKind(kind) -- I'll end up with a kind of "" when I hit the end of the string if kind == "" then return nil end -- Have I already cached a result from an earlier call? if cache[kind] then return cache[kind] end -- Split off the first piece from the rest of the kind. If the -- incoming kind is "list:key:string", thisKind will be "list" -- and nextKind will be "key:string". local thisKind = kind:match('(.-):') or kind local nextKind = kind:sub(#thisKind + 2) -- Get the processor function for this kind. Processors perform -- data validation and storage appropriate for the data structure. local functions = field._kinds[thisKind] if not functions then return nil, "Invalid field kind '" .. thisKind .. "'" end local processor = functions[method] if not processor then return nil end -- Now recurse to get the accessor function for the remaining parts -- of the field's data kind. If the kind was "list:key:string", then -- the processor function handles the "list" part, and this function -- takes care of the "key:string" part. local nextAccessor = accessorForKind(nextKind) -- Now here's the magic: wrap the processor and the next accessor -- up together into a Matryoshka doll of function calls, each call -- handling just it's level of the kind. accessor = function(f, current, value) return processor(f, current, value, nextAccessor) end -- And cache the result so I don't have to go through that again cache[kind] = accessor return accessor end return accessorForKind(f._kind) end function field.compare(f, a, b) local processor = field.accessor(f, "compare") if processor then return processor(f, a, b) else return (a == b) end end --- -- Fetch a field description by name. --- function field.get(name) return field._list[name] or field._loweredList[name:lower()] end function field.merge(f, current, value) local processor = field.accessor(f, "merge") if processor then return processor(f, current, value) else return value end end --- -- Is this a field that supports merging values together? Non-merging fields -- can simply overwrite their values, merging fields can call merge() to -- combine two values together. --- function field.merges(f) return (field.accessor(f, "merge") ~= nil) end --- -- Retrieve a property from a field, based on it's data kind. Allows extra -- information to be stored along with the data kind definitions; use this -- call to find the first value in the field's data kind chain. --- function field.property(f, tag) local kinds = string.explode(f._kind, ":", true) for i, kind in ipairs(kinds) do local value = field._kinds[kind][tag] if value ~= nil then return value end end end --- -- Override one of the field kind accessor functions. This works just like -- p.override(), but applies the new function to the internal field -- description and clears the accessor caches to make sure the change gets -- picked up by future operations. --- function field.override(fieldName, accessorName, func) local kind = field.kind(fieldName) p.override(kind, accessorName, func) field._accessors = {} end function field.remove(f, current, value) local processor = field.accessor(f, "remove") if processor then return processor(f, current, value) else return value end end function field.removes(f) return (field.accessor(f, "merge") ~= nil and field.accessor(f, "remove") ~= nil) end function field.store(f, current, value) local processor = field.accessor(f, "store") if processor then return processor(f, current, value) else return value end end function field.translate(f, value) local processor = field.accessor(f, "translate") if processor then return processor(f, value, nil)[1] else return value end end function field.translates(f) return (field.accessor(f, "translate") ~= nil) end