diff --git a/convert/convert.go b/convert/convert.go index 91a7d91..35d6c1d 100644 --- a/convert/convert.go +++ b/convert/convert.go @@ -641,6 +641,10 @@ func ToUint[T UintType](src any, def ...T) T { return toSingle(src, def...) } +func ToInteger[T IntegerType](src any, def ...T) T { + return toSingle(src, def...) +} + func ToFloat[T FloatType](src any, def ...T) T { return toSingle(src, def...) } diff --git a/ini/ini.go b/ini/ini.go new file mode 100644 index 0000000..672c0ec --- /dev/null +++ b/ini/ini.go @@ -0,0 +1,240 @@ +package ini + +import ( + "bufio" + "io" + "os" + "sort" + "strings" + + "gitea.zaclys.com/bvaudour/gob/convert" + . "gitea.zaclys.com/bvaudour/gob/option" +) + +/* +Setting représente le contenu d’un fichier de configuration de la forme .ini. +Un fichier .ini a la forme suivante : + + [section1] + clé1 = valeur + clé2 = valeur + ; un commentaire éventuel + #un autre commentaire + [section2] + clé1 = valeur + <...> + +L’accès aux clés de la configuration se fait en préfixant la clé souhaitée par la section +d’appartenance suivie d’un point (exemple : section1.clé1). + +Les types de valeurs supportées actuellement sont : +- les chaînes de caractère +- les booléens : 0, 1, true, false +- les entiers +- les tableaux de chaînes : ce sont des chaînes séparées par des virgules + +La conversion entre ces différents types se fait sans panic et aucun contrôle n’est effectué sur la cohérence des données. +*/ +type Setting struct { + t []string + m map[string]string + i map[string]int +} + +// New crée une nouvelle configuration à partir d’un template. +// Le template doit avoir la structure d’un fichier .ini. +func New(template string) *Setting { + c := &Setting{ + m: make(map[string]string), + i: make(map[string]int), + } + b := bufio.NewScanner(strings.NewReader(template)) + var section string + i := -1 + for b.Scan() { + i++ + line := b.Text() + c.t = append(c.t, line) + line = strings.TrimSpace(line) + l := len(line) + // Line is comment or blank line + if l == 0 || line[0] == '#' || line[0] == ';' { + continue + } + // line is section header + if line[0] == '[' && line[l-1] == ']' { + section = line[1 : l-1] + continue + } + if idx := strings.Index(line, "="); idx > 0 { + key, value := strings.TrimSpace(line[:idx]), strings.TrimSpace(line[idx+1:]) + k := section + "." + key + c.m[k] = value + c.i[k] = i + } + } + + return c +} + +// Keys retourne la liste des clés de la configuration. +// Chaque clé est des la forme section.clé. +func (c *Setting) Keys() []string { + out := make([]string, 0, len(c.i)) + for i := range c.i { + out = append(out, i) + } + sort.Slice(out, func(i, j int) bool { + return c.i[out[i]] < c.i[out[j]] + }) + + return out +} + +// Position retourne le numéro de ligne dans laquelle se trouve la clé donnée. +// Les n° de ligne commencent à partir de 0. +func (c *Setting) Position(key string) (position Option[int]) { + if p, ok := c.i[key]; ok { + position = Some(p) + } + + return +} + +func conv(e any) string { + var s string + t := convert.TypeOf(e) + switch { + case t.Is(convert.Bool): + s = "0" + if b := convert.ToBool(e, false); b { + s = "1" + } + case t.Is(convert.Slice): + a := convert.ToSlice[string](e) + s = strings.Join(a, ",") + default: + s = convert.ToString(e, "") + } + return s +} + +// Set définit la clé key à la valeur value. +// Si la clé n’existe pas, aucune modification n’est effectuée et la méthode retourne false. +func (c *Setting) Set(key string, value any) (ok bool) { + if _, ok = c.m[key]; ok { + c.m[key] = conv(value) + } + return +} + +// Get retourne la valeur de la clé demandée. +// Si celle-ci n’existe pas, une chaîne vide est retournée. +func (c *Setting) Get(key string) string { return c.m[key] } + +// GetBool retourne la valeur de la clé demandée, convertie en booléen. +func (c *Setting) GetBool(key string) (out bool) { + out = convert.ToBool(c.Get(key), false) + return +} + +// GetInt retourne la valeur de la clé demandée, convertie en entier. +func (c *Setting) GetInt(key string) (out int) { + out = convert.ToInt(c.Get(key), 0) + return +} + +// GetUint retourne la valeur de la clé demandée, convertie en entier non signé. +func (c *Setting) GetUint(key string) (out uint) { + out = convert.ToUint(c.Get(key), uint(0)) + return +} + +// GetSlice retourne la valeur de la clé demandée, convertie en slice. +func (c *Setting) GetSlice(key string) (out []string) { + v := c.Get(key) + if len(v) > 0 { + out = strings.Split(v, ",") + } + return +} + +// Read parse la configuration du descripteur d’entrée. +func (c *Setting) Read(r io.Reader) { + b := bufio.NewScanner(r) + var section string + for b.Scan() { + line := b.Text() + line = strings.TrimSpace(line) + l := len(line) + // Line is comment or blank line + if l == 0 || line[0] == '#' || line[0] == ';' { + continue + } + // line is section header + if line[0] == '[' && line[l-1] == ']' { + section = line[1 : l-1] + continue + } + if i := strings.Index(line, "="); i > 0 && i < l-1 { + key, value := strings.TrimSpace(line[:i]), strings.TrimSpace(line[i+1:]) + k := section + "." + key + c.m[k] = value + } + } +} + +// Write écrit la configuration dans le descripteur de sortie. +func (c *Setting) Write(w io.Writer) error { + for k, v := range c.m { + l := c.t[c.i[k]] + i := strings.Index(l, "=") + c.t[c.i[k]] = l[:i+1] + " " + v + } + buf := bufio.NewWriter(w) + for _, l := range c.t { + if _, err := buf.WriteString(l + "\n"); err != nil { + return err + } + } + return buf.Flush() +} + +// IniFile représente un fichier de configuration. +type IniFile struct { + *Setting + Path string +} + +// NewFile génère un nouveau fichier de de configuration. +// Rien n’est inscrit sur le chemin donné et cela reste en mémoire +// tant que la méthode Save n’est pas appelée explicitement. +// Par ailleurs, le fichier dans le filepath n’est pas parsé +// tant que la méthode Load n’est pas appelée explicitement. +func NewFile(template, filepath string) *IniFile { + return &IniFile{ + Setting: New(template), + Path: filepath, + } +} + +// Load charge la configuration +func (cf *IniFile) Load() error { + f, err := os.Open(cf.Path) + if err != nil { + return err + } + defer f.Close() + cf.Setting.Read(f) + return nil +} + +// Save enregistre la configuration sur le disque. +func (cf *IniFile) Save() error { + f, err := os.Create(cf.Path) + if err != nil { + return err + } + defer f.Close() + return cf.Setting.Write(f) +}