camellia

package module
v1.0.7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 7, 2022 License: GPL-2.0 Imports: 13 Imported by: 0

README

camellia 💮 A lightweight, persistent, hierarchical key-value store

camellia is a Go library that implements a hierarchical, persistent key-value store, backed by a SQLite database.
Its minimal footprint (just a single .db file) makes it suitable for usage in embedded systems, or simply as a minimalist application settings container.
Additionally, this repository contains the companion cml command line utility, useful to read, write and import/export a camellia DB.
The project was born to be the system-wide settings registry of a Linux embedded device, similar to the one found in Windows.


Library

API at a glance

package examples

import (
	"fmt"
	"os"

	cml "github.com/debevv/camellia"
)

func main() {
	_, err := cml.Open("/home/debevv/camellia.db")
	if err != nil {
		fmt.Printf("Error initializing camellia - %v", err)
		os.Exit(1)
	}

	// Set a string value
	cml.Set("status/userIdentifier", "ABCDEF123456")

	// Set a boolean value
	cml.Set("status/system/areWeOk", true)

	// Set a float value
	cml.Set("sensors/temperature/latestValue", -48.0)

	// Set an integer value
	cml.Set("sensors/saturation/latestValue", 99)

	// Read a single float64 value
	temp, err := cml.Get[float64]("sensors/temperature/latestValue")
	fmt.Printf("Last temperature is: %f", temp)

	// Read a single bool value
	ok, err := cml.Get[bool]("sensors/temperature/latestValue")
	fmt.Printf("Are we ok? %t", ok)

	// Delete an entry and its children
	err = cml.Delete("sensors")

	// Read a tree of entries
	sens, err := cml.GetEntry("sensors")
	fmt.Printf("Timestamp of last update of saturation value: %v", sens.Children["saturation"].LastUpdate)

	// Export whole DB as JSON
	j, err := cml.ValuesToJSON("")
	fmt.Printf("All DB values:\n%s", j)

	// Import DB from JSON file
	file, err := os.Open("db.json")
	cml.SetValuesFromJSON(file, false)

	// Register a callback called after a value is set
	cml.SetPostSetHook("status/system/areWeOk", func(path, value string) error {
		if value == "true" {
			fmt.Printf("System went back to normal")
		} else {
			fmt.Printf("Something bad happened")
		}

		return nil
	}, true)

	// Close the DB
	cml.Close()
}

API reference

https://pkg.go.dev/github.com/debevv/camellia

Installation and prerequisites

Prerequisites
  • Go 1.18 or greater, since this module makes use of generics
  • A C compiler and libsqlite3, given the dependency to go-sqlite3
Installation

Inside a module, run:

go get github.com/debevv/camellia

Overview

Entries

The data model is extremely simple.
Every entity in the DB is ab Entry. An Entry has the following properties:

Path       string
LastUpdate time.Time
IsValue    bool

When IsValue == true, the Entry carries a value, and it's a leaf node in the hierarchy. Values are always represented as strings:

Value string

When IsValue == false, the Entry does not carry a value, but it can have Children. It is the equivalent of a directory in a file system:

Children map[string]*Entry

This leads to the complete definition an Entry:

type Entry struct {
	Path       string
	LastUpdate time.Time
	IsValue    bool
	Value      string
	Children   map[string]*Entry
}
Paths

Paths are defined as strings separated by slashes (/). At the moment of writing this document, no limits are imposed to the length of a segment or to the length of the full path.
The root Entry is identified by an empty string.
When specifying a path, additional slashes are automatically ignored, so, for example

/my/path

or

///my///path//

are equivalent to

my/path

and an an empty string is equivalent to / or ////.

Database versioning and migration

The schema of the DB is versioned, so after updating the library, Init() may return ErrDBVersionMismatch. In this case, you should perform the migration of the DB by calling Migrate().

Setting and forcing

When setting a value, if a an Entry at that path already exists, but it's a non-value Entry, the operation fails.
Forcing a value instead will first delete the existing Entry (and all its children), and then replace it with the new value.

Concurrency

The library API should be safe to be called by different goroutines.
Regarding the usage of the same DB from different processes, it should be safe too, but more details will be added in the future (TBD).

Types

The internal data format for Entries' values is string. For this reason, the library API offers a set of methods that accept a type parameter and automatically serializes/deserializes values to/from string. Example:

// Gets the value at `path` and converts it to T
func Get[T Stringable](path string) (T, error)

// Converts `value` from T to `string` and sets it at `path`
func Set[T Stringable](path string, value T) error

The constraint of the type parameter is the Stringable interface:

type Stringable interface {
	BaseType
}

that in turn is composed by the BaseType interface, the collection of almost all Go supported base types.
Data satisfying the BaseType interface is serialized using fmt.Sprint() and deserialized using fmt.Scan.

Note on custom types

The library defines an additional interface for serialization:

type CustomStringable interface {
	String() string
	FromString(s string) error
}

intended to be used as a base for user-defined serializable types.
Unfortunately, support to custom types is not implemented at the moment, since go 1.18 does not allow to define Stringable in this way:

type Stringable interface {
  BaseType | CustomStringable
}

since unions of interfaces defining methods are not supported for now.

Please refer to this comment for more details.

JSON import/export

Formats

Entries can be imported/exported from/to JSON.
Two different formats are supported:

  • Default: meant to represent just the hierarchical relationship of Entries and their values. This will be the format used in most cases:
{
  "status": {
    "userIdentifier": "ABCDEF123456",
    "system": {
      "areWeOk": "true"
    }
  },
  "sensors": {
    "temperature": {
      "lastValue": "-48.0"
    },
    "saturation": {
      "lastValue": "99"
    }
  }
}

This format is used by the following methods:

func SetValuesFromJSON(reader io.Reader, onlyMerge bool) error
func ValuesToJSON(path string) (string, error)
  • Extended: carrying the all the properties of each Entry. The format was created to accommodate any future addition of useful metadata:
{
  "status": {
    "last_update_ms": "1641488635512",
    "children": {
      "userIdentifier": {
        "last_update_ms": "1641488675539",
        "value": "ABCDEF123456"
      },
      "system": {
        "last_update_ms": "1641453675583",
        "children": {
          "areWeOk": {
            "last_update_ms": "1641488659275",
            "value": "true"
          }
        }
      }
    }
  },
  "sensors": {
    "last_update_ms": "1641453582957",
    "children": {
      "temperature": {
        "last_update_ms": "1641453582957",
        "children": {
          "lastValue": {
            "last_update_ms": "1641453582957",
            "value": "-48.0"
          }
        }
      },
      "saturation": {
        "last_update_ms": "1641453582957",
        "children": {
          "lastValue": {
            "last_update_ms": "1641453582957",
            "value": "99"
          }
        }
      }
    }
  }
}

This format is used by the following methods:

func SetEntriesFromJSON(reader io.Reader, onlyMerge bool) error
func EntriesToJSON(path string) (string, error)

A note on last_update_ms: this property will be put in the JSON when exporting, but ignored when importing. The value of this property will be set to the timestamp of the actual moment of setting the Entry.

Import and merge

When importing from JSON, two distinct modes of operation are supported:

  • Import: the default operation. Overwrites any existing value with the one found in the input JSON. When overwriting, it forces values instead of just attempting to set them.
  • Merge: like import, but does not overwrite existing values with the ones found in the input JSON

Hooks

Hooks are callback methods that can be registered to run before (pre) and after (post) the setting of a certain value:

// Register a pre set hook to check the value before it is set
cml.SetPreSetHook("sensors/temperature/saturation", func(path, value string) error {
    saturation, err := strconv.Atoi(value)
    if err != nil {
        return fmt.Errorf("invalid saturation value")
    }

    // Block the setting of the value if it's out of range
    if saturation < 0 || saturation > 100 {
        return fmt.Errorf("invalid saturation value. Must be a percentage value")
    }

    return nil
})

// Register an async post set hook and react to changes
cml.SetPostSetHook("status/system/areWeOk", func(path, value string) error {
    if value == "true" {
        fmt.Printf("System went back to normal")
    } else {
        fmt.Printf("Something bad happened")
    }

    return nil
}, true)

Hooks can be synchronous or asynchronous:

  • Synchronous hooks are run on the same thread calling the Set() method. They can block the setting of a value by returning a non-nil error.
  • Asynchronous hooks are run on a new goroutine, and their return value is ignored (so the can't block the setting). Only post set hooks can be asynchronous.

cml command

Command line at a glance

# Set some values
cml set status/userIdentifier "ABCDEF123456"
cml set /status/system/areWeOk "true"
cml set "sensors/saturation/latestValue" 99
cml set sensors/temperature/latestValue "-48.0"

# Get a value
cml get sensors/temperature/latestValue
# -48.0

# Get some values
cml get sensors

# {
#   "saturation": {
#       "latestValue": "99"
#   },
#   "temperature": {
#       "latestValue": "-48.0"
#   }
# }

# Get Entries in the extended format
cml get -e sensors/temperature

# {
#    "last_update_ms": "1641453582957",
#    "children": {
#      "lastValue": {
#        "last_update_ms": "1641453582957",
#        "value": "-48.0"
#      }
#    }
# }

# Try to get a value, fail if it's a non-value
cml get -v sensors
# Error getting value - path is not a value

# Merge values from JSON file
cml merge /path/to/file.json

Installation

Install cml globally with:

go install github.com/debevv/camellia/cml@latest

Output of cml help

cml - The camellia hierarchical key-value store utility
Usage:
cfg get [-e] [-v] <path>        Displays the configuration entry (and its children) at <path> in JSON format
                                -e        Displays entries in the extended JSON format
                                -v        Fails (returns nonzero) if the entry is not a value
cfg set [-f] <path> <value>     Sets the configuration entry at <path> to <value>
                                -f        Forces overwrite of non-value entries
cfg delete <path>               Deletes a configuration entry (and its children)
cfg import [-e] <file>          Imports config entries from JSON <file>
                                -e        Use the extended JSON format
cfg merge [-e] <file>           Imports only non-existing config entries from JSON <file>
                                -e        Use the extended JSON format
cfg migrate                     Migrates the DB to the current supported version
cfg wipe [-y]                   Wipes the DB
                                -y        Does not ask for confirmation
cfg help                        Displays this help message

Database path

cml attempts to automatically determine the path of the SQLite database by reading it from different sources, in the following order:

  • From the CAMELLIA_DB_PATH environment variable, then
  • From the file /tmp/camellia.db.path, then
  • If the steps above fail, the path used is ./camellia.db

Documentation

Overview

camellia is a lightweight, persistent, hierarchical key-value store.

Its minimal footprint (just a single SQLite .db file) makes it suitable for usage in embedded systems, or simply as a minimalist application settings container.

For more info about usage and examples, see the README at https://github.com/debevv/camellia

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrPathInvalid       = errors.New("invalid path")
	ErrPathNotFound      = errors.New("path not found")
	ErrPathIsNotAValue   = errors.New("path is not a value")
	ErrValueEmpty        = errors.New("value is empty")
	ErrNoDB              = errors.New("no DB currently opened")
	ErrDBVersionMismatch = errors.New("DB version mismatch")
)

Functions

func Close

func Close() error

Close closes a camellia DB.

func Delete

func Delete(path string) error

Delete recursively deletes the Entry at the specified path and its children, if any.

func EntryToJSON

func EntryToJSON(path string) (string, error)

ValuesToJSON represents the hierarchy of Entries at the specified path in the extended JSON format.

func Exists

func Exists(path string) (bool, error)

Exists returns whether an Entry exists at the specified path.

func Force

func Force[T Stringable](path string, value T) error

Force sets a value of type T to the specified path.

If a non-value Entry already exists at the specified path, it is deleted first.

func ForceOrPanic

func ForceOrPanic[T Stringable](path string, value T)

ForceOrPanic calls Force with the specified parameters, and panics in case of error.

func Get

func Get[T Stringable](path string) (T, error)

Get reads the value a the specified path and returns it as type T.

func GetDBPath

func GetDBPath() string

GetDBPath returns the path of the current open DB.

func GetOrPanic

func GetOrPanic[T Stringable](path string) T

GetOrPanic calls Get with the specified parameters, and panics in case of error.

func GetOrPanicEmpty

func GetOrPanicEmpty[T Stringable](path string) T

GetOrPanic calls Get with the specified parameters, and panics if the read value is empty or in case of error.

func GetSupportedDBSchemaVersion

func GetSupportedDBSchemaVersion() uint64

GetSupportedDBSchemaVersion returns the current supported DB schema version.

func IsOpen

func IsOpen() bool

IsOpen returns whether a DB is currently open.

func Migrate

func Migrate(dbPath string) (bool, error)

Migrate migrates a DB at dbPath to the current supported DB schema version.

Returns true if the DB was actually migrated, false if it was already at the current supported DB schema version.

func Open

func Open(path string) (bool, error)

Open initializes a camellia DB for usage.

Most of the API methods will return ErrNoDB if Open is not called first.

func Recurse

func Recurse(path string, depth int, cb func(entry *Entry, parent *Entry, depth uint) error) error

Recurse recurses, breadth-first, the hierarchy of Entries at the specified path, starting with the Entry at the path.

For each entry, calls the specified callback with the Entry itself, its parent Entry, and the current relative depth in the hierarchy, with 0 being the depth of the Entry at the specified path.

func Set

func Set[T Stringable](path string, value T) error

Set sets a value of type T to the specified path.

func SetEntriesFromJSON

func SetEntriesFromJSON(reader io.Reader, onlyMerge bool) error

SetValuesFromJSON set (forces) the values found in the extended JSON representation read from reader.

If onlyMerge == true, does not overwrite an Entry with the one found in the JSON, if it already exists in the DB.

func SetHooksEnabled

func SetHooksEnabled(enabled bool)

func SetOrPanic

func SetOrPanic[T Stringable](path string, value T)

SetOrPanic calls Set with the specified parameters, and panics in case of error.

func SetPostSetHook

func SetPostSetHook(path string, callback func(path string, value string) error, async bool) error

SetPreSetHook registers a callback to be called after the value at the specified path is changed.

If async == false, if one of the registered callbacks on a path returns an error, the setting of the value at that path fails.

If async == true, the registered callback will be called inside a new goroutine, and its returned error is ignored.

Callback are always called in the same order as they were registered.

func SetPreSetHook

func SetPreSetHook(path string, callback func(path string, value string) error) error

SetPreSetHook registers a callback to be called before the value at the specified path is changed.

If one of the registered callbacks on a path returns an error, the setting of the value at that path fails.

Callbacks are called on the same thread executing the set operation, in the same order as they were registered.

func SetValuesFromJSON

func SetValuesFromJSON(reader io.Reader, onlyMerge bool) error

SetValuesFromJSON set (forces) the values found in the JSON representation read from reader.

If onlyMerge == true, does not overwrite an Entry with the value found in the JSON, if it already exists in the DB.

func ValuesToJSON

func ValuesToJSON(path string) (string, error)

ValuesToJSON represents the hierarchy of values at the specified path in the default JSON format.

func Wipe

func Wipe() error

Wipe deletes every Entry in the database, except for the root one (at path "").

Types

type BaseType

type BaseType interface {
	~int | ~uint | ~int8 | ~uint8 | ~int32 | ~uint32 | ~int64 | ~uint64 | ~float32 | ~float64 | ~bool | ~string
}

BaseType is the type set of built-in types accepted by Get/Set functions

type Entry

type Entry struct {
	Path       string
	LastUpdate time.Time
	IsValue    bool
	Value      string
	Children   map[string]*Entry
}

Entry represents a single node in the hierarchical store.

When IsValue == true, the Entry carries a value, and it's a leaf node in the hierarchy.

When IsValue == false, the Entry does not carry a value, but its Children map can contain Entires.

func GetEntry

func GetEntry(path string) (*Entry, error)

GetEntry returns the Entry at the specified path, including the eventual full hierarchy of children Entries.

func GetEntryDepth

func GetEntryDepth(path string, depth int) (*Entry, error)

GetEntry returns the Entry at the specified path, including the eventual hierarchy of children Entries, but stopping at a specified depth.

With depth > 0, stops at the specified depth.

With depth == 0, returns the Entry with an empty Children map.

With depth < 0, returns the full hierarchy of children Entries.

func (Entry) MarshalJSON

func (e Entry) MarshalJSON() ([]byte, error)

func (*Entry) UnmarshalJSON

func (e *Entry) UnmarshalJSON(b []byte) error

type Stringable

type Stringable interface {
	BaseType
}

Stringable is the type set of types accepted by Get/Set functions

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL