Jump to content

Module:ConfigRepository

Documentation for this module may be created at Module:ConfigRepository/doc

-- Module:ConfigRepository
-- Single source of truth for all template configurations across the ICANNWiki ecosystem
--
-- This module centralizes configuration data to ensure consistency, eliminate duplication, 
-- and provide standardized structure for all templates. Works with the Template Blueprint 
-- Framework to power rendering and semantic processing.
--
-- This module consists of three main parts:
-- 1. Global constants and properties shared across templates
-- 2. Template-specific configurations with standardized structure
-- 3. Configuration access functions for retrieving and standardizing configurations
--
-- Each template configuration provides a standardized structure with the following components:
--
-- meta:        Template metadata including description and rendering options
-- categories:  Base and conditional categories for the template
-- patterns:    Regular expression patterns for validation and parsing
-- fields:      Maps template field names in article source to display labels
-- mappings:    Canonical mappings for normalizing user input values
-- constants:   Template-specific constant values
-- semantics:   Semantic MediaWiki integration with the following sub-components:
--   properties:           Maps semantic properties to template fields (1:1 mapping)
--   additionalProperties: Maps properties to multiple possible template fields
--   transforms:           Functions to transform field values before storing as properties
--   skipProperties:       Properties to exclude from automatic processing

local p = {}
local ErrorHandling = require('Module:ErrorHandling')
local ConfigHelpers = require('Module:ConfigHelpers')

------------------------------------------------------------------------------
-- Global Constants and Properties
------------------------------------------------------------------------------

-- Global field labels used across multiple templates
-- These provide consistent labeling for common fields across all templates
-- and ensure that changes to label text only need to be made in one place
p.fieldLabels = {
    -- Geographic information
    region = "Region",
    country = "Country",
    city = "City",
    
    -- Temporal information
    date = "Date",
    start = "Start",
    ending = "End",

    -- Contact information
    website = "Website"
    -- Add others as needed
}

-- Global date formatting configuration
p.dateFormatting = {
    useShortMonthNames = true -- When true, use "Apr. 21, 2025" format; when false, use "April 21, 2025"
}

-- Global semantic property names used across multiple templates
-- For cross-template semantic queries
p.semanticProperties = {
    -- Geographic properties
    region = "Has ICANN region",
    country = "Has country",
    
    -- Person-related properties
    language = "Knows language",
    person = "Has person"
    -- Add others as needed
}

-- Transform registry mapping string keys to functions
p.transformRegistry = {
    date = function(value)
        return tostring(require('Module:NormalizationDate').formatDate(value))
    end,
    normalize = function(value, mappings)
        -- Return original if mappings invalid
        if type(mappings) ~= 'table' or #mappings == 0 then
            return value
        end
        local CanonicalForms = require('Module:CanonicalForms')
        local canonical = select(1, CanonicalForms.normalize(value, mappings))
        return canonical or value
    end,
    semantic = function(value, templateName)
        return require('Module:TemplateHelpers').extractSemanticValue(value, templateName)
    end,
    boolean = function(value)
        if value and value ~= "" then
            return "true"
        end
    end,
    raw = function(value) return value end,
    string = function(value) return tostring(value or "") end
}

------------------------------------------------------------------------------
-- TEMPLATE-SPECIFIC CONFIGURATIONS
------------------------------------------------------------------------------
p.templates = {

------------------------------------------------------------------------------
-- ANCHOR: ORGANIZATION TEMPLATE
------------------------------------------------------------------------------

Organization = {
    meta = {
        description = "Organization template"
    },
    
    categories = { -- Default categories
        base = {"Organization"}
    },
    
    fields = {
        {key="logo", label="Logo"}, -- REVIEW
        {key="organization_type", label="Type"},
        {key="focus", label="Focus"},
        {key="region", label=p.fieldLabels.region},
        {key="country", label=p.fieldLabels.country},
        {key="city", label=p.fieldLabels.city},
        {key="date_founded", label="Founded"},
        {key="founders", label="Founders"},
        {key="ownership", label="Ownership", autoWikiLink=true},
        {key="subsidiaries", label="Subsidiaries"},
        {keys={"website", "url"}, label=p.fieldLabels.website},
    },
    
    semantics = {
        properties = {
            ["Has organization type"] = "organization_type",
            ["Has focus"] = "focus",
            ["Has city"] = "city",
            ["Date founded"] = "date_founded",
            ["Has owner"] = "ownership",
            ["Has subsidiary"] = "subsidiaries"
        },

        -- Normalizations applied
        transforms = {
            -- None
        },
        
        skipProperties = { -- REVIEW
            -- No properties to skip
        }
    }
},

------------------------------------------------------------------------------
-- ANCHOR: EVENT TEMPLATE
------------------------------------------------------------------------------

    Event = {
        meta = {
            description = "Event template"
        },
        
        categories = { -- Default categories
            base = {"Events"}
        },

        patterns = {
            -- Patterns for event navigation detection
            seriesNumber = "^([%w%s]+)%s+(%d+)$",     -- e.g., "ICANN 76"
            seriesYear = "^([%w%s]+)%s+(%d%d%d%d)$"  -- e.g., "IGF 2023"
        },
        
        fields = {
            {key="logo", label="Logo"},
            {key="process", label="Process", autoWikiLink=true},
            {key="start", label=p.fieldLabels.start},
            {key="ending", label=p.fieldLabels.ending},
            {key="region", label=p.fieldLabels.region},
            {key="country", label=p.fieldLabels.country},
            {key="city", label="City"},
            {key="venue", label="Venue"},
            {key="organizer", label="Organizer"},
            {keys={"website", "url"}, label=p.fieldLabels.website},
        },
        
        semantics = {
            properties = {
                ["Has start date"] = "start",
                ["Has end date"] = "ending",
                ["Has city"] = "city",
                ["Has venue"] = "venue",
                ["Has event organizer"] = "organizer"
            },

            -- Normalizations applied
            transforms = {
                ["Has start date"] = "date",
                ["Has end date"] = "date"
            },
            
            skipProperties = {
                -- No properties to skip
            }
        }
    },

------------------------------------------------------------------------------
-- ANCHOR: LIBRARY INTERVIEW TEMPLATE
------------------------------------------------------------------------------

LibraryInterview = {
    meta = {
        description = "Library Interview Template"
    },
    
    categories = { -- Default categories
        base = {
            "Internet & Digital Governance Library",
            "ICANNWiki Interviews"
        }
    },
    
    constants = {
        title = "Internet & Digital Governance Library",
        tableClass = "library-box"
    },
    
    fields = {
        {key="title", label="Title"},
        {key="format_interview", label="Format"},
        {key="date", label=p.fieldLabels.date},
        {key="interviewer", label="Interviewer", autoWikiLink=true},
        {key="interviewee", label="Interviewee", autoWikiLink=true},
        {key="id", label="Permanent ID"}
    },
    
    semantics = {
        -- Direct property mappings (1:1 relationship between property and field)
        properties = {
            ["Has interview format"] = "format_interview",
            ["Has date"] = "date",
            ["Has interviewer"] = "interviewer", 
            ["Has interviewee"] = "interviewee",
            ["Permanent ID"] = "id"
        },
        
        -- Properties that map to multiple possible fields
        additionalProperties = {
            ["Has person"] = {"interviewer", "interviewee"} -- Links interviews to person pages
        },
        
        -- Normalizations applied
        transforms = {
            ["Has date"] = "date",
            ["Permanent ID"] = "string"
        },
        
        skipProperties = {
            -- No properties to skip
        }
    }
},

------------------------------------------------------------------------------
-- ANCHOR: PERSON TEMPLATE
------------------------------------------------------------------------------

    Person = {
        meta = {
            description = "Person template"
        },
        
        categories = { -- Default categories
            base = {"Person"}
        },
        
        mappings = {
            -- "Community" mappings for normalizing user input to canonical forms
            -- Each entry maps various synonyms to a canonical form and associated category
            community = {
                -- ICANN-related communities
                {canonical = "ICANN Community",
                 synonyms = {"icann", "community"},
                 category = "ICANN Community"},
                {canonical = "ICANN Staff",
                 synonyms = {"staff", "icann org"},
                 category = "ICANN Staff"},
                
                -- Technical communities
                {canonical = "Root Server Operator Community",
                 synonyms = {"root server operator", "rso"},
                 category = "Root Server Operator Community"},
                {canonical = "RIR Community",
                 synonyms = {"rir"},
                 category = "RIR Community"},
                {canonical = "Universal Acceptance Community",
                 synonyms = {"universal acceptance", "ua", "ua member", "idn", "idn community"},
                 category = "Universal Acceptance Community"},
                
                -- Internet governance organizations
                {canonical = "ISOC Community",
                 synonyms = {"isoc", "internet society", "internet society community", "isoc member"},
                 category = "ISOC Community"},
                {canonical = "IETF Community",
                 synonyms = {"ietf", "ietf member"},
                 category = "IETF Community"},
                {canonical = "W3C Community",
                 synonyms = {"w3c", "w3c member"},
                 category = "W3C Community"},
                {canonical = "IGF Community",
                 synonyms = {"igf", "nri", "youth igf"},
                 category = "IGF Community"},
                
                -- Government-related
                {canonical = "Governmental",
                 synonyms = {"government"},
                 category = "Governmental"},
                {canonical = "Intergovernmental",
                 synonyms = {"igo"},
                 category = "Intergovernmental"}
            }
        },
        
        fields = {
            {key="community", label="Community"},
            {key="icann_group", label="ICANN group", autoWikiLink=true},
            {key="organization", label="Organization", autoWikiLink=true},
            {key="region", label=p.fieldLabels.region},
            {key="country", label=p.fieldLabels.country},
            {key="languages", label="Languages"},
            {key="website", label=p.fieldLabels.website},
            {key="soi", label="SOI"},
            {key="userbox", label="Achievements"},
            {key="user", label="User", hidden=true}
        },

        patterns = {
            itemDelimiter = ";%s*",  -- Matches semicolon followed by optional whitespace; used for splitting multi-value fields
            websitePattern = "^https?://[^%s]+"  -- Matches URLs starting with http:// or https:// followed by non-whitespace chars
        },
        
        semantics = {
            properties = {
                ["Has governance community"] = "community",
                ["Has ICANN affiliation"] = "icann_group",
                ["Has organization"] = "organization",
                ["Has username"] = "user"
            },
            
            -- Normalizations applied
            transforms = {
                ["Has governance community"] = "normalize",
                ["Knows language"] = "raw"
            },
            
            skipProperties = { 
                -- No properties to skip
            }
        }
    },

------------------------------------------------------------------------------
-- ANCHOR: PROCESS TEMPLATE
------------------------------------------------------------------------------

    Process = {
        meta = {
            description = "Process Template",
            templateId = "process"
        },
        
        categories = {
            base = {"Processes", "Events"}
        },
        
        mappings = {
            -- Process type mappings
            model = {
                {canonical = "Multistakeholder", 
                 synonyms = {"multi-stakeholder", "multi stakeholder"}, 
                 category = "Multistakeholder Processes"},
                {canonical = "Single stakeholder", 
                 synonyms = {"single-stakeholder", "single stakeholder", "intergovernmental"}, 
                 category = "Single Stakeholder Processes"}
            },
            
            -- Status mappings
            status = {
                {canonical = "Active", 
                 synonyms = {"active", "ongoing"}, 
                 category = "Active Processes"},
                {canonical = "Paused", 
                 synonyms = {"paused", "inactive", "dormant"}, 
                 category = "Dormant Processes"},
                {canonical = "Concluded", 
                 synonyms = {"concluded", "completed", "ended"}, 
                 category = "Concluded Processes"}
            },
            
            -- Frequency mappings
            frequency = {
                {canonical = "One-off", 
                 synonyms = {"one-off", "one off", "single"}, 
                 category = "One-off Processes"},
                {canonical = "Ad-hoc", 
                 synonyms = {"ad-hoc", "ad hoc", "eventual"},
                 category = "Ad-hoc Processes"},
                {canonical = "Periodic", 
                 synonyms = {"periodic", "recurring", "regular"},
                 category = "Periodic Processes"},
                {canonical = "Continuous", 
                 synonyms = {"continuous", "ongoing", "permanent"}, 
                 category = "Continuous Processes"}
            },
            
            -- Scope mappings
            scope_geo = {
                {canonical = "Global", 
                 synonyms = {"global", "worldwide", "international"}, 
                 category = "Global Processes"},
                {canonical = "Regional", 
                 synonyms = {"regional", "continent", "continental"}, 
                 category = "Regional Processes"},
                {canonical = "National", 
                 synonyms = {"national", "country", "domestic"}, 
                 category = "National Processes"}
            }
        },
        
        fields = {
            {key="logo", label="Logo"},
            {key="process", label="Process", autoWikiLink=true},
            {key="model", label="Model"},
            {key="start", label=p.fieldLabels.start},
            {key="ending", label=p.fieldLabels.ending},
            {key="process_status", label="Status"},
            {key="governing_body", label="Governing Body", autoWikiLink=true},
            {key="secretariat", label="Secretariat", autoWikiLink=true},
            {key="frequency", label="Frequency"},
            {key="scope_geo", label="Scope"},
            {key="country", label=p.fieldLabels.country},
            {key="website", label=p.fieldLabels.website},
            {key="has_previous", label="Preceded By", hidden=true},
            {key="has_next", label="Succeeded By", hidden=true},
        },
        
        semantics = {
            properties = {

                ["Has start date"] = "start",
                ["Has end date"] = "ending",
                ["Has process status"] = "process_status",
                ["Has governing body"] = "governing_body",
                ["Has event organizer"] = "secretariat",
                ["Has process model"] = "model",
                ["Has frequency"] = "frequency",
                ["Has geographic scope"] = "scope_geo",
                ["Has process connection"] = "process"
            },
            
            additionalProperties = {
                ["Has process connection"] = {"process", "has_previous", "has_next"}
            },
            
            -- Normalizations applied
            transforms = {
                ["Has start date"] = "date",
                ["Has end date"] = "date",
                ["Has process status"] = "normalize",
                ["Has process model"] = "normalize",
                ["Has frequency"] = "normalize",
                ["Has geographic scope"] = "normalize",
                ["Has process connection"] = "semantic"
            },
            
            skipProperties = {
                -- No properties to skip
            }
        }
    },
    
------------------------------------------------------------------------------
-- ANCHOR: TLD TEMPLATE
------------------------------------------------------------------------------

    TLD = {
        meta = {
            description = "TLD Template"
        },
        
        categories = {
            base = {}, -- No default category for TLDs as they use type-based categories
            conditional = {
                rvc = "TLDs with RVCs",
                idn = "IDN",
                idn_cctld = "IDN ccTLD"
            },
        },
        
        mappings = {
            tld_type = {
                {canonical = "gTLD",
                 synonyms = {"generic", "tld", "generic top level domain", "generic top-level domain", "generic tld"},
                 category = "gTLD"},
                {canonical = "ccTLD",
                 synonyms = {"country", "cc", "country code top level domain", "country code top-level domain", "country tld"},
                 category = "ccTLD"}
            },
            tld_subtype = {
                {canonical="geoTLD",
                 synonyms={"geo tld","geo","geographic","geographical","geographic top level domain","geographic top-level domain","geographic tld"},
                 css="tld-template-subtype-geotld",
                 category="geoTLD"},
                {canonical="dotBrand",
                 synonyms={"brand","brandtld","brand tld","brand top level domain","brand top-level domain"},
                 css="tld-template-subtype-brandtld",
                 category="dotBrand"},
                {canonical="Sponsored TLD",
                 synonyms={"sponsored","sponsored top level domain","sponsored top-level domain"},
                 css="tld-template-subtype-sponsored",
                 category="Sponsored TLD"},
                {canonical="Legacy TLD",
                 synonyms={"legacy","legacy top level domain","legacy top-level domain"},
                 css="tld-template-subtype-legacytld",
                 category="Legacy TLD"},
                {canonical="2012 gTLD Round",
                 synonyms={"gtld round 2012","2012 ngtld round","2012 ngtld","ngtld 2012","ngtld","2012"},
                 css="tld-template-subtype-ngtld-round-2012",
                 category="2012 gTLD Round"}
            }
        },

        patterns = {
            tldExtension="%.([^%.]+)$",  -- Extracts the TLD part after the last dot (e.g., "com" from "example.com")
            countryDelimiter="([^;]+)"   -- Matches any sequence of characters not containing semicolons; used for country parsing
        },
        
        fields = {
            {key="tld_type", label="Type"},
            {key="tld_subtype", label="Subtype"},
            {key="tld_status", label="Status"},
            {key="region", label=p.fieldLabels.region},
            {key="country", label=p.fieldLabels.country},
            {key="introduced", label="Introduced"},
            {key="implemented", label="Implemented"},
            {key="script", label="Script"},
            {key="language", label="Language"},
            {key="translation", label="In English"},
            {key="ascii", label="Punycode"},
            {key="registry", label="Registry", autoWikiLink=true},
            {key="website", label=p.fieldLabels.website},
            {key="iso31661a2", label = "ISO 3166-1 alpha-2", hidden=true},
            {keys={"RVC", "PIC"}, label = "PIC/RVC"}
        },
        
        semantics = {
            properties = {
                ["Has TLD type"] = "tld_type",
                ["Has TLD subtype"] = "tld_subtype",
                ["Has TLD status"] = "tld_status",
                ["Date introduced"] = "introduced",
                ["Date implemented"] = "implemented",
                ["Uses writing script"] = "script",
                ["Has registry operator"] = "registry",
                ["Has language"] = "language",
                ["Has ISO 3166-1 alpha-2 code"] = "iso31661a2",
                ["Has PIC or RVC"] = "RVC", -- Will also handle PIC
                ["Is IDN"] = "idn", -- Boolean
            },
            
            additionalProperties = {
                ["Has PIC or RVC"] = {"RVC", "PIC"}
            },
            
            -- Normalizations applied
        transforms = {
                ["Has TLD type"] = "normalize",
                ["Has TLD subtype"] = "normalize",
                ["Date introduced"] = "date",
                ["Date implemented"] = "date",
                ["Has PIC or RVC"] = "boolean",
                ["Is IDN"] = "boolean",
            },
            
            skipProperties = {
                -- No properties to skip
            }
        }
    }
}

------------------------------------------------------------------------------
-- CONFIGURATION ACCESS FUNCTIONS
------------------------------------------------------------------------------

-- Get configuration for a specific template type

function p.getConfig(templateType)
    return p.templates[templateType] or {}
end

-- Local references to ConfigHelpers functions for better readability
local deepMerge = ConfigHelpers.deepMerge
local fieldHasKey = ConfigHelpers.fieldHasKey

-- Creates a standardized configuration structure for template modules
function p.createStandardConfig(config, customOverrides, templateType)
    config = config or {}
    customOverrides = customOverrides or {}
    
    -- Initialize with defaults from config
    local standardConfig = {
        meta = config.meta or {
            description = "Template module configuration"
        },
        mappings = config.mappings or {},
        fields = config.fields or {},
        semantics = config.semantics or {
            properties = {},
            transforms = {},
            additionalProperties = {}
        },
        constants = config.constants or {},
        patterns = config.patterns or {},
        categories = config.categories or {} -- Add categories field to preserve base categories
    }
    
    -- Deep-merge customOverrides into standardConfig
    standardConfig = deepMerge(standardConfig, customOverrides)
    
    -- Auto-inject country/region semantic mappings if fields exist
    if standardConfig.fields and #standardConfig.fields > 0 then
        local hasCountryField = false
        local hasRegionField = false
        
        -- Check if country or region fields exist
        for _, field in ipairs(standardConfig.fields) do
            if fieldHasKey(field, "country") or fieldHasKey(field, "territory") then
                hasCountryField = true
            end
            
            if fieldHasKey(field, "region") then
                hasRegionField = true
            end
        end
        
        -- Initialize additionalProperties if it doesn't exist
        standardConfig.semantics.additionalProperties = standardConfig.semantics.additionalProperties or {}
        
        -- Auto-inject country mapping if a country field exists and not skipped
        if hasCountryField and not (standardConfig.semantics.skipProperties and standardConfig.semantics.skipProperties[p.semanticProperties.country]) then
            standardConfig.semantics.additionalProperties[p.semanticProperties.country] = 
                standardConfig.semantics.additionalProperties[p.semanticProperties.country] or {"country", "territory"}
        end
        
        -- Auto-inject region mapping if a region field exists and not skipped
        if hasRegionField and not (standardConfig.semantics.skipProperties and standardConfig.semantics.skipProperties[p.semanticProperties.region]) then
            standardConfig.semantics.additionalProperties[p.semanticProperties.region] = 
                standardConfig.semantics.additionalProperties[p.semanticProperties.region] or {"region"}
        end
    end

    -- Inject built-in transform functions for declarative transforms
    for propertyName, transformIdentifier in pairs(standardConfig.semantics.transforms) do
        if type(transformIdentifier) == "string" then
            local registryFn = p.transformRegistry[transformIdentifier]
            if registryFn then
                standardConfig.semantics.transforms[propertyName] = function(value)
                if transformIdentifier == "normalize" then
                    local mappingKey = standardConfig.semantics.properties[propertyName]
                    local mappingTable = standardConfig.mappings[mappingKey]
                    if not mappingTable then
                        local suffix = mappingKey:match(".*_(.*)")
                        mappingTable = suffix and standardConfig.mappings[suffix] or mappingTable
                    end
                    return registryFn(value, mappingTable)
                elseif transformIdentifier == "semantic" then
                        return registryFn(value, templateType)
                    else
                        return registryFn(value)
                    end
                end
            else
                ErrorHandling.addError(
                    ErrorHandling.createContext(templateType),
                    "TransformInjection",
                    "Unknown transform type",
                    transformIdentifier,
                    false
                )
            end
        end
    end

    return standardConfig
end

-- Get a standard config object for use with existing template modules
function p.getStandardConfig(templateType, customOverrides)
    -- Get the base configuration
    local baseConfig = p.getConfig(templateType)
    
    -- Create a standard config
    return p.createStandardConfig(baseConfig, customOverrides, templateType)
end

return p