Skip to content

Commit

Permalink
Merge pull request #1 from codescalersinternships/development
Browse files Browse the repository at this point in the history
INI parser
  • Loading branch information
RawanMostafa08 authored Jan 13, 2025
2 parents 775c826 + 2b7743a commit dbc44fd
Show file tree
Hide file tree
Showing 6 changed files with 726 additions and 1 deletion.
30 changes: 30 additions & 0 deletions .github/workflows/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Go CI

on: [push]

jobs:
golangci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: v1.60
- name: Go Format
uses: Jerome1337/[email protected]
with:
gofmt-path: './src'
gofmt-flags: '-l -d'
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
- name: Test with the Go CLI
run: go test -v ./pkg
61 changes: 60 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,60 @@
# RawanMostafa-inigo
# INI Config Parser

This repository implements an ini config parser

## In this README 👇

- [Features](#features)
- [Usage](#usage)

## Features
- `InitParser()` returns a parser object to call our APIs
- `LoadFromString()` takes a string ini configs and parses them into the caller iniParser object
- `LoadFromFile()` takes an ini file path and parses it into the caller iniParser object
- `GetSectionNames()` returns an array of strings having the names of the sections of the caller iniParser object
- `GetSections()` returns a representing the parsed data structure of the caller iniParser object
- `Get()` takes a sectionName and a key and returns the value of this key and an error if found
- `Set()` takes a sectionName, a key and a value, it sets the passed key with the passed value and returns an error if found
- `ToString()` returns the parsed ini map of the caller object as one string
- `SaveToFile()` takes a path to an output file, saves the parsed ini map of the caller object into a file and returns an error if found

## Usage

1.
```go
import github.com/codescalersinternships/RawanMostafa-inigo
```

2. Initialize the parser first
```go
parser := InitParser()
```

3. Example usage:
```go
parser.LoadFromString(s)
```
```go
parser.LoadFromFile(filepath)
```
```go
names := parser.GetSectionNames()
```
```go
sections := parser.GetSections()
```
```go
value, err := parser.Get(sectionName, key)
```
```go
err = parser.Set(sectionName, key, value)
```
```go
s := parser.ToString()
```
```go
err = parser.SaveToFile(outPath)
```



3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/codescalersinternships/RawanMostafa-inigo

go 1.23.1
195 changes: 195 additions & 0 deletions pkg/iniparser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// Package iniparser implements a parser for ini files
//
// The iniparser package should only be used for for ini files and doesn't support any other types of files
package iniparser

import (
"errors"
"fmt"
"os"
"sort"
"strings"
)

var ErrNoKey = errors.New("key not found")
var ErrNoSection = errors.New("section not found")
var ErrNotINI = errors.New("this is not an ini file")
var ErrGlobalKey = errors.New("global keys aren't supported")
var ErrFileNotExist = errors.New("file doesn't exist")

// The Parser acts as the data structure storing all of the parsed sections
type Parser struct {
sections map[string]map[string]string
}

// NewParser returns a Parser type object
// NewParser is an essential call to get a parser to be able to access its APIs
func NewParser() Parser {
return Parser{
make(map[string]map[string]string),
}
}

func (i Parser) parse(lines []string) error {
var title string
var sec map[string]string
inSection := false
for _, line := range lines {

line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, ";") || strings.HasPrefix(line, "#") {
continue
}
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
inSection = true
if title != "" {
i.sections[title] = sec
}
title = strings.Trim(line, "[]")
sec = make(map[string]string)

} else if strings.Contains(line, "=") {
if !inSection {
return ErrGlobalKey
}
parts := strings.SplitN(line, "=", 2)
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
sec[key] = value
}
}
if title != "" {
i.sections[title] = sec
}
return nil
}

// LoadFromString is a method that takes a string ini configs and parses them into the caller Parser object
// LoadFromString assumes that:
// 1- There're no global keys, every keys need to be part of a section
// 2- The key value separator is just =
// 3- Comments are only valid at the beginning of the line
func (i Parser) LoadFromString(data string) (err error) {

lines := strings.Split(data, "\n")
err = i.parse(lines)
return
}

// LoadFromFile is a method that takes a path to an ini file and parses it into the caller Parser object
// LoadFromFile assumes that:
// 1- There're no global keys, every keys need to be part of a section
// 2- The key value separator is just =
// 3- Comments are only valid at the beginning of the line
func (i Parser) LoadFromFile(path string) error {

if !strings.HasSuffix(path, ".ini") {
return ErrNotINI
}

data, err := os.ReadFile(path)
if err != nil {
return ErrFileNotExist
}
err = i.LoadFromString(string(data))
if err != nil {
return err
}
return nil
}

// GetSectionNames is a method that returns an array of strings having the names
// of the sections of the caller Parser object and an error in case of empty sections
func (i Parser) GetSectionNames() ([]string, error) {
if len(i.sections) == 0 {
return make([]string, 0), nil
}
names := make([]string, 0)
for key := range i.sections {
names = append(names, key)
}
sort.Strings(names)
return names, nil
}

// GetSections is a method that returns a map[string]section representing
// the data structure of the caller Parser object and an error in case of empty sections
func (i Parser) GetSections() (map[string]map[string]string, error) {
if len(i.sections) == 0 {
return make(map[string]map[string]string, 0), nil
}
return i.sections, nil
}

// Get is a method that takes a string for the sectionName and a string for the key
// and returns the value of this key and a boolean to indicate if found or not
func (i Parser) Get(sectionName string, key string) (string, bool) {

val, exists := i.sections[sectionName][key]
return val, exists
}

// Set is a method that takes a string for the sectionName, a string for the key and a string for the value of this key
// It sets the passed key if found with the passed value and returns an error
// Error formats for different cases:
//
// If the section name passed isn't found --> "section not found"
// If the key passed isn't found in the passed section --> "key not found"
// else --> nil
func (i Parser) Set(sectionName string, key string, value string) error {
if _, ok := i.sections[sectionName]; !ok {
return ErrNoSection
}
if _, ok := i.sections[sectionName][key]; !ok {
return ErrNoKey
}
i.sections[sectionName][key] = value
return nil
}

// String is a method that returns the parsed ini map of the caller object as one string
// The returned string won't include the comments
// Also,it tells fmt pkg how to print the object
func (i Parser) String() string {
sectionNames := make([]string, 0, len(i.sections))
for sectionName := range i.sections {
sectionNames = append(sectionNames, sectionName)
}
sort.Strings(sectionNames)

var b strings.Builder
for _, sectionName := range sectionNames {
keys := make([]string, 0, len(i.sections[sectionName]))
b.WriteString(fmt.Sprintf("[%s]\n", sectionName))
for key := range i.sections[sectionName] {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
b.WriteString(fmt.Sprintf("%s = %s\n", key, i.sections[sectionName][key]))
}
}
return b.String()
}

// SaveToFile is a method that takes a path to an output file and returns an error
// It saves the parsed ini map of the caller object into a file
// Error formats for different cases:
//
// If the file couldn't be opened --> "error in opening the file:"
// If writing to the file failed --> "error in writing to the file:"
// else --> nil
func (i Parser) SaveToFile(path string) error {
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return fmt.Errorf("error in opening the file: %w", err)
}
defer file.Close()

stringFile := i.String()
_, err = file.WriteString(stringFile)
if err != nil {
return fmt.Errorf("error in writing to the file: %w", err)
}
return nil
}
Loading

0 comments on commit dbc44fd

Please sign in to comment.