pasta_lua 0.2.1

Pasta Lua - Lua integration for Pasta DSL
Documentation
local standards = {}

-- A standard (aka std) defines set of allowed globals, their fields,
-- and whether they are mutable.
--
-- A standard can be in several formats. Internal (normalized) format
-- is a tree. Each node defines a global or its field. Each node may have
-- boolean `read_only` and `other_fields`, and may contain definitions
-- of nested fields in `fields` subtable, which maps field names
-- to definition tables. For example, standard defining globals
-- of some Lua version may start like this:
-- {
--    -- Most globals are read-only by default.
--    read_only = true,
--    fields = {
--       -- The tree can't be recursive, just allow everything for `_G`.
--       _G = {other_fields = true, read_only = false},
--       package = {
--          fields = {
--             -- `other_fields` is false by default, so that an empty table
--             -- defines a field that can't be indexed further (a function in this case).
--             loadlib = {},
--             -- Allow doing everything with `package.loaded`.
--             loaded = {other_fields = true, read_only = false},
--             -- More fields here...
--          }
--       },
--       -- More globals here...
--    }
-- }
--
-- A similar format is used to define standards in table form
-- in config. There are two differences:
-- first, top level table can have two fields, `globals` and `read_globals`,
-- that map global names to definition tables. Default value of `read_only` field
-- for the these tables depends on which table they come from (`true` for `read_globals`
-- and `false` for `globals`). Additionally, all tables that map field or global names
-- to definition tables may have non-string keys, their associated values are interpreted
-- as names instead and their definition table allows indexing with any keys indefinitely.
-- E.g. `{fields = {"foo"}}` is equivalent to `{fields = {foo = {other_fields = true}}}`.
-- This feature makes it easier to create less strict standards that do not care about fields,
-- to ease migration from the old format.
--
-- Additionally, there are some predefined named standards in `luacheck.builtin_standards` module.
-- In config and inline options its possible to use their names as strings to refer to them.

-- Validates an optional table mapping field names to field definitions or non-string keys to names.
-- `index` is an optional string specifying position of the field table in the root table.
-- Returns a true if the table is valid, false, an error message, and index of the table with the error otherwise.
local function validate_fields(fields, is_root, index)
   if fields == nil then
      return true
   end

   local field_type = is_root and "global" or "field"

   if type(fields) ~= "table" then
      return false, ("%ss table expected, got %s"):format(field_type, type(fields)), index
   end

   for key, value in pairs(fields) do
      if type(key) == "string" then
         local new_index = (index or "") .. "." .. key

         if type(value) ~= "table" then
            return false, ("%s description table expected, got %s"):format(field_type, type(value)), new_index
         end

         if value.read_only ~= nil and type(value.read_only) ~= "boolean" then
            local err = "invalid value of option 'read_only': boolean expected, got " .. type(value.read_only)
            return false, err, new_index
         end

         if value.other_fields ~= nil and type(value.other_fields) ~= "boolean" then
            local err = "invalid value of option 'other_fields': boolean expected, got " .. type(value.other_fields)
            return false, err, new_index
         end

         local ok, err, err_index = validate_fields(value.fields, false, new_index .. ".fields")

         if not ok then
            return false, err, err_index
         end
      elseif type(value) ~= "string" then
         local key_as_string = type(key) == "number" and ("%.20g"):format(key) or ("<%s>"):format(type(key))
         local new_index = ("%s[%s]"):format(index or "", key_as_string)
         return false, ("string expected as %s name, got %s"):format(field_type, type(value)), new_index
      end
   end

   return true
end

-- Validates a field table.
-- Returns true if the table is valid, false and an error message otherwise.
function standards.validate_globals_table(globals_table)
   local ok, err, err_index = validate_fields(globals_table, true)

   if ok then
      return true
   end

   local err_prefix = err_index and ("in field %s: "):format(err_index) or ""
   return false, err_prefix .. err
end

-- Validates an std table in user-side format.
-- Returns true if the table is valid, false and an error message otherwise.
function standards.validate_std_table(std_table)
   local ok, err, err_index = validate_fields(std_table.globals, true, ".globals")

   if ok then
      ok, err, err_index = validate_fields(std_table.read_globals, true, ".read_globals")
   end

   if ok then
      return true
   end

   local err_prefix = ("in field %s: "):format(err_index)
   return false, err_prefix .. err
end

local infinitely_indexable_def = {other_fields = true}

local function add_fields(def, fields, overwrite, ignore_array_part, default_read_only)
   if not fields then
      return
   end

   for field_name, field_def in pairs(fields) do
      if type(field_name) == "string" or not ignore_array_part then
         if type(field_name) ~= "string" then
            field_name = field_def
            field_def = infinitely_indexable_def
         end

         if not def.fields then
            def.fields = {}
         end

         if not def.fields[field_name] then
            def.fields[field_name] = {}
         end

         local existing_field_def = def.fields[field_name]
         local new_read_only = field_def.read_only

         if new_read_only == nil then
            new_read_only = default_read_only
         end

         if new_read_only ~= nil then
            if overwrite or new_read_only == false then
               existing_field_def.read_only = new_read_only
            end
         end

         if field_def.other_fields ~= nil then
            if overwrite or field_def.other_fields == true then
               existing_field_def.other_fields = field_def.other_fields
            end
         end

         add_fields(existing_field_def, field_def.fields, overwrite, false, nil)
      end
   end
end

-- Merges in an std table in user-side format.
-- By default the new state of normalized std is a union of the standard tables being merged,
-- e.g. if either table allows some field to be mutated, result should allow it, too.
-- If `overwrite` is truthy, read-only statuses from the new std table overwrite existing values.
-- If `ignore_top_array_part` is truthy, non-string keys in `globals` and `read_globals` tables
-- in `std_table` are not processed.
function standards.add_std_table(final_std, std_table, overwrite, ignore_top_array_part)
   add_fields(final_std, std_table.globals, overwrite, ignore_top_array_part, false)
   add_fields(final_std, std_table.read_globals, overwrite, ignore_top_array_part, true)
end

-- Overwrites or adds definition of a field with given read-only status and any nested keys.
-- Field is specified as an array of field names.
function standards.overwrite_field(final_std, field_names, read_only)
   local field_def = final_std

   for _, field_name in ipairs(field_names) do
      if not field_def.fields then
         field_def.fields = {}
      end

      if not field_def.fields[field_name] then
         field_def.fields[field_name] = {read_only = read_only}
      end

      field_def = field_def.fields[field_name]
   end

   for key in pairs(field_def) do
      field_def[key] = nil
   end

   field_def.read_only = read_only
   field_def.other_fields = true
end

-- Removes definition of a field from a normalized std table.
-- Field is specified as an array of field names.
function standards.remove_field(final_std, field_names)
   local field_def = final_std
   local parent_def

   for _, field_name in ipairs(field_names) do
      parent_def = field_def

      if not field_def.fields or not field_def.fields[field_name] then
         -- The field wasn't defined in the first place.
         return
      end

      field_def = field_def.fields[field_name]
   end

   if parent_def then
      parent_def.fields[field_names[#field_names]] = nil
   end
end

local function infer_deep_read_only_statuses(def, read_only)
   local deep_read_only = not def.other_fields or read_only

   if def.fields then
      for _, field_def in pairs(def.fields) do
         local field_read_only = read_only

         if field_def.read_only ~= nil then
            field_read_only = field_def.read_only
         end

         infer_deep_read_only_statuses(field_def, field_read_only)
         deep_read_only = deep_read_only and field_read_only and field_def.deep_read_only
      end
   end

   if deep_read_only then
      def.deep_read_only = true
   end
end

-- Finishes building a normalized std tables.
-- Adds `deep_read_only` fields with `true` value to definition tables
-- that do not have any writable fields, recursively.
function standards.finalize(final_std)
   infer_deep_read_only_statuses(final_std, true)
end

local empty = {}

-- Returns a definition table containing empty fields with given names.
function standards.def_fields(...)
   local fields = {}

   for _, field in ipairs({...}) do
      fields[field] = empty
   end

   return {fields = fields}
end

return standards