[CLASS] ConfMan - Ini Management Class

Post your working scripts, libraries and tools.
User avatar
cyruz
Posts: 347
Joined: 30 Sep 2013, 13:31

[CLASS] ConfMan - Ini Management Class

17 Jan 2023, 18:09

Hi guys,

due to my recent switch to V2 and the need to dig deep into its object implementation, I decided to rewrite the function I use to manage the configuration in my scripts into a class.

This is basically a class that can be used to configure an object representing the content of a "ini" file and manipulate its data with provided and custom functions.

To make the best out of it and learn during the process, I experimented with delegation, meta functions and proxying (incorrectly labeling one of the class as "interface"). I know this problem was basically a solved one, but this has to be considered a pure didactic exercise.

Code: Select all

; ----------------------------------------------------------------------------------------------------------------------
; Class ........: ConfMan
; Description ..: Ini files management class, implementing an object central storage and an interface dealing with the
; ..............: underlined classes. ConfMan.IniInterface filters and redirect requests and calls to ConfMan.IniRoot,
; ..............: ConfMan.IniSection and ConfMan.IniFuncs.
; ..............: IniRoot and IniSections are basically Maps objects where the property notation is translated to item 
; ..............: notation, allowing to define a complete "ini" file structure like an object literal.
; ..............: ConfMan is a static class. Do not instantiate, use the available public methods to interact with it.
; Pub. Method ..: ConfMan.GetConf(sFileName, funcs*)
; Description ..: It initializes the object central storage and returns a ConfMan.IniInterface object.
; Parameters ...: sFileName - "Ini" file full path. If the file itself is not existing, it can be created with
; ..............:             a write operation. If the path to the file is not valid, an error will be thrown.
; ..............: funcs*    - Variadic parameter allowing for a series of function objects to be injected into the
; ..............:             ConfMan.IniFunc class. Injected functions must accept 2 parameters: "this" and the
; ..............:             ConfMan.IniRoot object used to process all "ini" file sections.
; Return .......: ConfMan.IniInterface object to be used for all interactions.
; Pub. Method ..: ConfMan.DiscardStorage()
; Description ..: Discard all object central storage.
; Interface ....: ConfMan.IniInterface (instantiated objects will be referred next as "objInterface")
; Description ..: objInterface allow the user to configure the object representing the "ini" file with literal notation
; ..............: and address the contained sections with property and item notations. objInterface "shadows" the
; ..............: ConfMan.IniRoot object and allow to interact with the ConfMan.IniSection objects.
; Obj. Config ..: Use literal notation: { SECTION1: {key1: value, key2: value}, SECTION2: {key1: value, key2: value} }
; ..............: Each section can be accessed as objInterface.SECTION1 or objInterface["SECTION1"].
; ..............: Each section can be configured with the "SetOpts" method with the following "options" (boolean flags):
; ..............: LOCKED  - This section should not be overridden by any operation.
; ..............: NOWRITE - This section is not written to file.
; ..............: NOEXKEY - Extra section keys will not be added to the configuration object when reading the file.
; ..............: PARAMS  - This section can be overridden by command line parameters.
; ..............: OBJECT  - When reading or overriding this section, the value in the key:value pair will be replaced by
; ..............:           a simple object with the "Value" property set (eg: { Value: value }).
; ..............: *** Please note that a section cannot have both the LOCKED and PARAMS options set.
; Pub. Method ..: objInterface.ReadFile()
; Description ..: Read file content and override all sections with the option LOCKED unset with the relative key:value 
; ..............: pairs.
; Pub. Method ..: objInterface.WriteFile()
; Description ..: Truncate the ini file if existing and create a new "ini" file with the ConfMan.IniRoot object content.
; Pub. Method ..: objInterface.ParseParams()
; Description ..: Parse command line parameters and override all sections with the option LOCKED unset and with the
; ..............: option PARAM set. The command line parameters must follow the notation [SECTION]key=value. The
; ..............: [SECTION] part can be omitted if only 1 section is marked with the PARAM option. If the value contains
; ..............: spaces, it can be enclosed in double quotes, that will be removed at processing time. New lines can be
; ..............: specified with the AutoHotkey notation `n.
; Pub. Method ..: objInterface.Section.SetOpts(sOptions)
; Description ..: Allow to specify a space separated list of option for the section. If the option is not present in the
; ..............: list, it will be set to 0 (eg: "LOCKED OBJECT" will set LOCKED=1, PARAMS=0, OBJECT=1).
; Parameters ...: sOpts - Space separated list containing the allowed options (LOCKED, PARAMS, OBJECT).
; Pub. Method ..: objInterface.Section.GetOpt(sOpt)
; Description ..: Get the value of the desired section option.
; Parameters ...: sOpt - Option to be retrieved (LOCKED, PARAMS, OBJECT).
; AHK Version ..: AHK v2 x32/64 Unicode
; Author .......: cyruz - http://ciroprincipe.info
; License ......: WTFPL - http://www.wtfpl.net/txt/copying/
; Changelog ....: Jan. 17, 2023 - v0.0.1 - First version.
; ..............: Jan. 18, 2023 - v0.0.2 - Fixed object override when OBJECT option is set and an object is already set.
; ..............:                          Added the NOWRITE option.
; ..............: Jan. 19, 2023 - v0.0.3 - Fixed an issue with the ReadFile function where the "Default" property of the
; ..............:                          ConfMan.IniRoot object was requested if the read Key was not present in the
; ..............:                          ConfMan.IniSection object.
; ..............: Jan. 22, 2023 - v0.0.4 - Added the NOEXKEY option.
; Thanks .......: swagfag - https://www.autohotkey.com/boards/memberlist.php?mode=viewprofile&u=75383
; ----------------------------------------------------------------------------------------------------------------------
; Class Diagram Description:
;
;  ┌───────┐
;  │ConfMan│
;  │       ├───────┬────────────────────────┬─────────────────────────┬──────────────────────┐
;  │Static │       │(parent)                │(parent)                 │(parent)              │(provide)
;  └───┬───┘       ▼                        ▼                         ▼                      ▼
;      │   ┌───────────────┐    ┌───────────────────────┐    ┌─────────────────┐    ┌ ─ ─ ─ ─ ─ ─ ─ ─ ┐        
;      │   │ConfMan.IniRoot│    │ConfMan.IniSection     │    │ConfMan.IniFuncs │    │Object Storage   │ 
;      │   │Extends Map    │◄───┤Extends ConfMan.IniRoot│    │                 │    │Map              │
;      │   │               │    │                       │    │Static           │    │Static Class Var.│ 
;      │   └───────────────┘    └───────────────────────┘    └─────────────────┘    └ ─ ─ ─ ─ ─ ─ ─ ─ ┘
;      │           ▲                        ▲                         ▲                      ▲
;      │           │(shadow)                │(shadow)                 │(forward calls)       │
;      │           └────────────────────────┼─────────────────────────┘                      │
;      │                                    │                                                │
;      │                         ┌──────────┴──────────┐                                     │
;      │(parent - return)        │ConfMan.IniInterface │      (store ConfMan.IniRoot objects)│
;      └────────────────────────►│Extends Buffer       ├─────────────────────────────────────┘
;                                │Interface            │
;                                └─────────────────────┘
;
; ----------------------------------------------------------------------------------------------------------------------

class ConfMan
{
    ; Object central storage.
    ; This will be instantiated as a Map().
    static objStor := ""
    
    static Call(params*)
    {
        throw Error("This is a static class. Please use <ConfMan.GetConf(sFileName)> to get an object.")
    }
    
    ; MAIN METHOD - Returns a ConfMan.IniInterface object after central storage initialization.
    static GetConf(sFileName, funcs*)
    {
        (!IsObject(ConfMan.objStor)) && ConfMan.objStor := Map()     
        return ConfMan.IniInterface(sFileName, funcs*)
    }
    
    ; Completely discard all central storage content.
    static DiscardStorage() => ConfMan.objStor := ""
    
    ; Collection of functions working on IniRoot objects.
    ; Custom functions can be injected, each function must accept 2 parameters:
    ; 1. this (to be ignored, it will be the ConfMan.IniInterface object).
    ; 2. oRoot (the ConfMan.IniRoot object to process all ConfMan.IniSection objects).
    class IniFuncs
    {
        static Call(params*)
        {
            throw Error("Static class.")
        }
        
        static ReadFile(oRoot)
        {
            sFileName := oRoot.__GetProp("FILENAME")
            
            if !FileExist(sFileName)
               throw Error("File not existing.", sFileName)
               
            sIniFile := IniRead(sFileName)
            loop parse, sIniFile, "`n"
            {
                if oRoot.Has(A_LoopField)
                {
                    if oRoot[A_LoopField].__GetProp("OPT_LOCKED")
                        continue
                    
                    sSectionName    := A_LoopField
                    sSectionContent := IniRead(sFileName, sSectionName)
                    bObject         := oRoot[A_LoopField].__GetProp("OPT_OBJECT")
                    bNoKeys         := oRoot[A_LoopField].__GetProp("OPT_NOEXKEY")
                    
                    loop parse, sSectionContent, "`n"
                    {
                        ; Match oM.1 = KEY / oM.2 = VALUE
                        if RegExMatch(A_LoopField, "S)^\s*(\w+)\s*\=\s*(.*)\s*$", &oM:=0)
                        {
                            if bNoKeys && !oRoot[sSectionName].Has(oM.1)
                                continue
                                
                            oM.2 := ConfMan.IniFuncs.__UnescapeNewLine(oM.2)
                            if bObject
                                (oRoot[sSectionName].Has(oM.1) && IsObject(oRoot[sSectionName][oM.1]))
                               ? oRoot[sSectionName][oM.1].Value := oM.2
                               : oRoot[sSectionName][oM.1] := { Value: oM.2 }
                            else oRoot[sSectionName][oM.1] := oM.2
                            bUpdated := 1
                        }
                    }
                }
            }
            
            return IsSet(bUpdated) ? 1 : 0
        }
        
        static WriteFile(oRoot)
        {
            sFileName := oRoot.__GetProp("FILENAME")
            
            try
            {
                f := FileOpen(sFileName, "w")
                for sec,cont in oRoot
                {
                    if cont.__GetProp("OPT_NOWRITE")
                        continue
                    f.WriteLine("[" sec "]")
                    for k,v in cont
                    {   ; If our IniSection key:value pair value is an object, get its "Value" property.
                        (IsObject(v) && v.HasOwnProp("Value")) && v := v.Value
                        f.WriteLine(k "=" ConfMan.IniFuncs.__EscapeNewLine(v))
                    }
                }
            }
            catch Error as e
                throw e
            finally
                (IsSet(f)) && f.Close()
            
            return 1
        }

        static ParseParams(oRoot)
        {
            ; Take note of the sections marked with the PARAMS option.
            sParamSec := "", nCount := 0
            for sec,cont in oRoot
                if cont.__GetProp("OPT_PARAMS")
                    sParamSec := sec, nCount++

            if !nCount
                throw ValueError("No section has been marked with the PARAMS option.")
            
            loop A_Args.Length
            {
                ; Match oM.1 = SECTION / oM.2 = KEY / oM.3 = VALUE
                if RegExMatch(A_Args[A_Index], "S)^(?:\[([^\[\]]+)\])*([\w]+)\=(.+)$", &oM:=0)
                {
                    ; Throw if:
                    ; * There are multiple sections marked to be overridden by parameters but none has been specified.
                    ; * A section has been specified but it does not exists or it is not marked to be overridden.
                    
                    if oM.1 == "" && nCount > 1
                        throw ValueError("Multiple section marked with PARAMS option but none has been specified.")
                    if oM.1 != "" && (!oRoot.Has(oM.1) || !oRoot[oM.1].__GetProp("OPT_PARAMS"))
                        throw ValueError("Section [" oM.1 "] does not exists or is not marked with PARAMS option.")
                    
                    ; If we have only one section marked with PARAMAS option we can avoid the
                    ; [SECTION] part in the command line parameter, so we perform this assignment.
                    (oM.1 == "") && oM.1 := sParamSec
                    
                    ; Do not throw if the section is marked with the NOEXKEY option, just skip the key.
                    if oRoot[oM.1].__GetProp("OPT_NOEXKEY") && !oRoot[oM.1].Has(oM.2)
                        continue
                    
                    ; Remove surrounding double quotes if present and unescape new lines.
                    (InStr(oM.3, "`"", 1) = 1) && (InStr(oM.3, "`"",, -1) = StrLen(oM.3)) && oM.3 := SubStr(oM.3, 2, -1)
                    oM.3 := ConfMan.IniFuncs.__UnescapeNewLine(oM.3)
                    
                    ; Perform the assignment.
                    if oRoot[oM.1].__GetProp("OPT_OBJECT")
                        (IsObject(oRoot[oM.1][oM.2]))
                       ? oRoot[oM.1][oM.2].Value := oM.3
                       : oRoot[oM.1][oM.2] := { Value: oM.3 }
                    else oRoot[oM.1][oM.2] := oM.3
                }
                else throw ValueError("Wrong parameter format: " A_Args[A_Index])
            }
        }
        
        static __EscapeNewLine(sText)   => StrReplace(sText, "`n", "``n")
        static __UnescapeNewLine(sText) => StrReplace(sText, "``n", "`n")
    }
    
    ; Implements a "Ini" file root.
    ; It manages __Get and __Set request blending the property and item notation.
    ; It defines the "Props" dynamic property and getters/setters to manage object properties.
    class IniRoot extends Map
    {    
        __New(params*)
        {
            if Mod(params.Length, 2) != 0
                throw ValueError("Constructor parameters are property,value pairs.")

            ; We use DefineProp to bypass __Get & __Set.
            this.DefineProp("Props", { Value: Map() })
            this.__SetProps(params*)
            
            super.__New()
            return this
        }
        
        ; Return an item, even if a property has been requested.
        __Get(name, params)
        {
            if this.Has(name)
                return params.Length > 0 ? this[name][params*] : this[name]
            else throw PropertyError("Key not found: " name)
        }
        
        ; Set an item, even if the property notation has been used.
        __Set(name, params, value) => params.Length > 0 ? this[name][params*] := value : this[name] := value
        
        ; We want the user to interact with "properties" only through methods.
        ; Due to the ConfMan.IniInterface object "shadowing" the ConfMan.IniRoot object and 
        ; forwarding method calls to the outer class, these methods will be accessible only if 
        ; using its "Root" prop (eg: <Root.Root.__GetProp("FILENAME")>).
        ; These same methods can, instead, be called directly on ConfMan.IniSection objects
        ; (eg: <Root["section"].__GetProp("NAME")> or <Root.section.__GetProp("NAME")>).
        
        __GetProp(name)        => this.Props[name]
        __GetProps()           => this.Props
        __SetProp(name, value) => this.Props[name] := value
        __SetProps(params*)
        {
            tmp := Map(), idx := 1
            Loop params.Length//2
            {
                prop := params[idx]
                tmp[prop] := params[idx+1]
                idx += 2
            }
            
            if !tmp.Has("FILENAME")
                throw ValueError("IniRoot objects need at least a FILENAME property.")
            
            if !FileExist(RegExReplace(tmp["FILENAME"], "[^\\]+$"))
                throw ValueError("File path not valid: " tmp["FILENAME"])
            
            this.Props := tmp
        }
    }
    
    ; Implements a "Ini" file section.
    ; ConfMan.IniSection class extends ConfMan.IniRoot adding the management of section options.
    ; ConfMan.IniSection "options" are boolean flags built on top of ConfMan.IniRoot properties.
    class IniSection extends ConfMan.IniRoot
    {
        __New(params*) => super.__New(params*)
        
        ; Override the __SetProps method to enforce specific checks.
        __SetProps(params*)
        {
            tmp := Map(), idx := 1
            Loop params.Length//2
            {
                prop := params[idx]
                tmp[prop] := params[idx+1]
                idx += 2
            }
            
            if !tmp.Has("NAME")
                throw ValueError("IniSection objects need at least a NAME property.")  
                
            this.Props := tmp
        }
        
        ; Build on top of __GetProp, to return a single option.
        GetOpt(sOpt) => this.__GetProp("OPT_" sOpt)
        
        ; Build on top of __SetProp to set a list of section options.
        ; If it's present in the string, set the relative option to 1, otherwise to 0.
        SetOpts(sOpts)
        {
            if InStr(sOpts, "LOCKED") && InStr(sOpts, "PARAMS")
                throw ValueError("IniSection objects can't be locked and overridden by parameters.")            

            for k in this.__GetProps()
                (RegExMatch(k, "OPT_([\w]+)", &oM:=0)) && this.__SetProp(k, InStr(sOpts, oM.1) ? 1 : 0)
        }
    }
    
    ; Acts as a proxy for all object interactions dispatching calls/requests to the sibling/parent classes.
    class IniInterface extends Buffer
    {
        __New(sFileName, funcs*)
        {
            ; "Inject" the function objects in the ConfMan.IniFuncs class if there are any.
            ; Each function must declare two parameters: "this" and the IniRoot object.
            loop funcs.Length
            {
                if IsObject(funcs[A_Index]) && funcs[A_Index].HasMethod()
                    ConfMan.IniFuncs.DefineProp(funcs[A_Index].name, { Call: funcs[A_Index] })
                else throw ValueError("funcs* items must be function objects.")
            }
            
            ; Initialize the Buffer properties.
            super.__New()
            
            ; We use the Buffer "Ptr" property as Map key for our objects central storage.
            ; That object will be removed at this object release by the __Delete meta function.
            ConfMan.objStor[this.Ptr] := ConfMan.IniRoot("FILENAME", sFileName)
            
            ; Define the Root property to have a ready access to the IniRoot object in the storage.
            this.DefineProp("Root", { Value: ConfMan.objStor[this.Ptr] })
            return this
        }
        
        ; Remove the object from the central storage.
        __Delete() => (IsObject(this.Root)) && ConfMan.objStor.Delete(this.Ptr)
        
        ; Forward method calls to the IniFuncs class.
        __Call(name, params) => ConfMan.IniFuncs.%name%(this.Root)
        
        ; Forward property requests to the IniRoot object.
        __Get(name, params) => this.Root.__Get(name, params)
        
        ; Create a "translation" layer that allows only selected operations. More specifically:
        ; * Allow <Root["section"]["key"] := value> notation.
        ; * Allow defining sections with the literal notation, translating properties to items:
        ;   <Root.section := { key1: value, ..., keyN: value }>
        
        __Set(name, params, value)
        {            
            ; Disallow setting non-object properties but only if there are no parameters [] in the call.
            ; This to allow item notation assignments for simple key:value pairs that will be managed next.
            
            if !params.Length && !IsObject(value)
                throw ValueError("IniRoot properties can be only objects defined with literal notation.")
            
            ; Disallow Root["section"]["param1",..,"paramN"] calls.
            if params.Length > 1
                throw PropertyError("IniSection properties are simple key:value pairs.")
            
            ; Create the IniSection object as IniRoot item in the central storage, if not present.
            (!this.Root.Has(name)) && this.Root[name] := ConfMan.IniSection( "NAME",        name
                                                                           , "OPT_LOCKED",  0
                                                                           , "OPT_NOWRITE", 0
                                                                           , "OPT_NOEXKEY", 0
                                                                           , "OPT_PARAMS",  0
                                                                           , "OPT_OBJECT",  0 )
            
            ; Allow item notation assignments (eg: <Root["section"]["key"] := value>).
            if params.Length
                return this.Root[name][params*] := value
            
            ; Translate object literal notation into item notation.
            for k,v in value.OwnProps()
                this.Root[name][k] := v
            
            ; Return for assignments chain.
            return this.Root[name]
        }
        
        ; Redirect __Item calls to __Get & __Set (eg: Root["section"]).
        __Item[params]
        {
            get => this.__Get(params, [])
            set => this.__Set(params, [], value)
        }
    }
}

/* Test Code:

#Include <Class_ConfMan>

a := ConfMan.GetConf("c:\test.ini", functest, functestobj)

a.WWWW :=
{
  WHAT  : "Nothing"
, WHO   : "No one" 
, WHERE : "Nowhere"
, WHEN  : "Never"
}

a.WWWW.SetOpts("OBJECT PARAMS")
a.WriteFile()

Msgbox a.functest()

a.ReadFile()

Msgbox a.functestobj()


functest(this, oRoot)
{
    return oRoot.WWWW.WHAT
}

functestobj(this, oRoot)
{
    return oRoot.WWWW.WHO.Value
}

*/

Changelog:
  • Jan. 18, 2023 - v0.0.2 - Fixed object override when OBJECT option is set and an object is already set. Added the NOWRITE option.
  • Jan. 19, 2023 - v0.0.3 - Fixed an issue with the ReadFile function where the "Default" property of the ConfMan.IniRoot object was requested if the read Key was not present in the ConfMan.IniSection object.
  • Jan. 22, 2023 - v0.0.4 - Added the NOEXKEY option.

Cheers :beer:
ABCza on the old forum.
My GitHub.
Jacquesyip
Posts: 5
Joined: 18 May 2023, 22:16
Contact:

Re: [CLASS] ConfMan - Ini Management Class

18 May 2023, 22:23

Hi, I have been use <ConfOverride> to load config, but I looking for a lib to manage Ini, which not only load config, but support initial an *.ini file with default config in the script, update some config through write to Ini.
And now, I found you provide this <ConfMan>, but I don't know how to use thi library, could you provide a example? thank you so much.
LazarouJoinery
Posts: 22
Joined: 02 Jan 2022, 21:41

Re: [CLASS] ConfMan - Ini Management Class

19 May 2023, 00:24

Jacquesyip wrote:
18 May 2023, 22:23
Hi, I have been use <ConfOverride> to load config, but I looking for a lib to manage Ini, which not only load config, but support initial an *.ini file with default config in the script, update some config through write to Ini.
And now, I found you provide this <ConfMan>, but I don't know how to use thi library, could you provide a example? thank you so much.
it's at the bottom of the lib
/*
ball-and-stick
commented
*/
Jacquesyip
Posts: 5
Joined: 18 May 2023, 22:16
Contact:

Re: [CLASS] ConfMan - Ini Management Class

19 May 2023, 02:56

LazarouJoinery wrote:
19 May 2023, 00:24
Jacquesyip wrote:
18 May 2023, 22:23
Hi, I have been use <ConfOverride> to load config, but I looking for a lib to manage Ini, which not only load config, but support initial an *.ini file with default config in the script, update some config through write to Ini.
And now, I found you provide this <ConfMan>, but I don't know how to use thi library, could you provide a example? thank you so much.
it's at the bottom of the lib
/*
ball-and-stick
commented
*/
Thank you very much and forgive my carelessness~ :thumbup:
Jacquesyip
Posts: 5
Joined: 18 May 2023, 22:16
Contact:

Sadly, It doesn't work properly in v2.1(alpha)

07 May 2024, 22:10

image.png
image.png (53.83 KiB) Viewed 379 times
User avatar
cyruz
Posts: 347
Joined: 30 Sep 2013, 13:31

Re: Sadly, It doesn't work properly in v2.1(alpha)

11 May 2024, 16:20

Jacquesyip wrote:
07 May 2024, 22:10
image.png
Does the same script work with the stable version? I'm lagging behind with Autohotkey updates and I don't know if 2.1 has any breaking changes or not.
ABCza on the old forum.
My GitHub.
Jacquesyip
Posts: 5
Joined: 18 May 2023, 22:16
Contact:

Re: Sadly, It doesn't work properly in v2.1(alpha)

14 May 2024, 22:17

cyruz wrote:
11 May 2024, 16:20
Jacquesyip wrote:
07 May 2024, 22:10
image.png
Does the same script work with the stable version? I'm lagging behind with Autohotkey updates and I don't know if 2.1 has any breaking changes or not.
Yep, I downgrade to 2.0.10 and it work fine! :D

Return to “Scripts and Functions (v2)”

Who is online

Users browsing this forum: No registered users and 22 guests