loam

package module
v0.10.2 Latest Latest
Warning

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

Go to latest
Published: Jan 7, 2026 License: AGPL-3.0 Imports: 5 Imported by: 2

README

Loam 🌱

An Embedded Reactive & Transactional Engine for Content & Metadata.

Go Report Card Go Doc License Release

Loam é uma engine reativa e transacional de documentos embutida, desenhada para aplicações centradas em conteúdo e metadados.

Por padrão, o Loam utiliza o Sistema de Arquivos + Git como banco de dados (.md, .yaml, .json, .csv), oferecendo controle de versão zero-config e legibilidade humana. No entanto, sua arquitetura Core é agnóstica, pronta para escalar para outros backends (S3, SQL) sem alterar o código do aplicativo.

É ideal para toolmakers que constroem:

  • Assistentes de PKM (Obsidian, Logseq) - Storage layer apenas.
  • Gerenciadores de Configuração (GitOps, Dotfiles).
  • Pipelines de Dados Locais (ETL de CSV/JSON).
  • Geradores de Sites Estáticos (Hugo, Jekyll).

🗺️ Navegação

🤔 Por que Loam?

Por que não apenas usar os.WriteFile ou SQLite?

  • Local-First & Soberania: Seus dados são simples arquivos de texto (.md, .json). Você tem total controle e não depende do Loam para acessá-los.
  • GitOps Nativo: Todo Save gera um histórico auditável. Reverta erros e gerencie estado de configuração com a mesma segurança de infraestrutura.
  • Automação Segura (ACID): Transações em lote e file-locking garantem que seus scripts de automação nunca corrompam o repositório.

📄 Arquivos Suportados (Smart Persistence)

O Adapter padrão (FS) detecta automaticamente o formato do arquivo baseado na extensão do ID, suportando leitura e escrita raw (--raw):

  • Markdown (.md): Padrão. Conteúdo + Frontmatter YAML.
  • JSON (.json): Serializa como objeto JSON puro. Campo content é opcional.
  • YAML (.yaml): Serializa como objeto YAML puro. Campo content é opcional.
  • CSV (.csv): Serializa como linha de valores. Suporta coleções com múltiplos documentos.

Smart Retrieval: Na leitura (Get), se o ID não tiver extensão (ex: dados), o Loam procura automaticamente por dados.md, dados.json, etc., respeitando a existência do arquivo.

🚀 Instalação

Via Go Install (Recomendado)
go install github.com/aretw0/loam/cmd/loam@latest
Via Release

Baixe os binários pré-compilados na página de Releases.

Compilando do Fonte (Build)

Para desenvolvedores, utilizamos make para simplificar o processo:

# Build para sua plataforma atual
make build

# Cross-compilation (Linux, Windows, Mac)
make cross-build

# Instalar localmente
make install
Executando Testes

Para rodar a suíte de testes (excluindo testes de stress que podem ser lentos no Windows):

# Windows (PowerShell)
go test -v ./pkg/... ./cmd/... ./internal/... ./tests/e2e ./tests/reactivity ./tests/typed

# Linux/Mac (via Makefile)
make test-fast

🛠️ CLI: Uso Básico

O Loam CLI funciona como um "Gerenciador de Conteúdo", abstraindo a persistência.

Inicializar

Inicia um cofre Loam. Por padrão usa o adapter de sistema de arquivos (FS + Git).

loam init
# Ou explicitamente:
loam init --adapter fs
Criar/Editar Documento

Salva conteúdo e registra a razão da mudança (Commits no caso do Git).

# Modo Simples (apenas mensagem)
loam write -id daily/2025-12-06 -content "Hoje foi um dia produtivo." -m "log diário"

# Modo Semântico (Type, Scope, Body)
loam write -id feature/nova-ideia -content "..." --type feat --scope ideias -m "adiciona rascunho"

# Modo Imperativo (--set)
# Define metadados individuais sem precisar de JSON
loam write --id docs/readme.md --content "Texto" --set title="Novo Readme" --set status=draft

# Modo Declarativo (--raw)
# Envie o documento inteiro via pipe. O Loam detecta Frontmatter/JSON/CSV.
echo '{"title":"Logs", "content":"..."}' | loam write --id logs/1.json --raw

[!NOTE] No modo --raw, se o ID não possuir extensão (ex: --id nota), a CLI assumirá .md por padrão para tentar parsear o conteúdo. Se estiver enviando JSON ou CSV sem extensão no ID, o parse falhará.

Sincronizar (Sync)

Sincroniza o cofre com o remoto configurado (se o adapter suportar).

loam sync
Outros Comandos
  • Ler: loam read -id daily/2025-12-06
  • Listar: loam list
  • Deletar: loam delete -id daily/2025-12-06

📦 Library: Uso em Go

Você pode embutir o Loam em seus próprios projetos Go para gerenciar persistência de dados.

go get github.com/aretw0/loam
Exemplo
package main

import (
 "context"
 "fmt"
 "log/slog"
 "os"

 "github.com/aretw0/loam/pkg/core"
 "github.com/aretw0/loam"
)

func main() {
 // 1. Inicializar Serviço (Factory) com Functional Options.
 // O primeiro argumento é a URI ou Path do cofre. Para o adapter FS, use o caminho do diretório.
 service, err := loam.New("./meus-docs",
  loam.WithAdapter("fs"), // Padrão
  loam.WithAutoInit(true), // Cria diretório e git init se necessário
  loam.WithLogger(slog.New(slog.NewTextHandler(os.Stdout, nil))),
 )
 if err != nil {
  panic(err)
 }

 ctx := context.Background()

 // 2. Escrever (Save)
 // Salvamos o conteúdo com uma "razão de mudança" (Commit Message)
 // Isso garante que toda mudança tenha um porquê.
 ctxMsg := context.WithValue(ctx, core.ChangeReasonKey, "documento inicial")
 err = service.SaveDocument(ctxMsg, "daily/hoje", "# Dia Incrível\nComeçamos o projeto.", nil)
 if err != nil {
  panic(err)
 }
 fmt.Println("Documento salvo com sucesso!")

 // 3. Ler (Read)
 doc, err := service.GetDocument(ctx, "daily/hoje")
 if err != nil { // Tratamento simplificado
  panic(err)
 }
 fmt.Printf("Conteúdo recuperado:\n%s\n", doc.Content)

 // ... (veja exemplos completos em examples/basics/crud)
}
Typed Retrieval (Generics)

Para maior segurança de tipos, você pode usar o wrapper genérico:

type User struct { Name string `json:"name"` }

// Abre um repositório já tipado (leitura/escrita de User)
// O ID do documento é preservado, mas o conteúdo é mapeado para User.
userRepo, err := loam.OpenTypedRepository[User]("./meus-docs")
if err != nil {
    panic(err)
}

// Acesso tipado
user, _ := userRepo.Get(ctx, "users/alice")
fmt.Println(user.Data.Name) // Type-safe!
Reactivity (Watch)

Você pode observar mudanças em repositórios tipados para implementar "Hot Reload" de configurações ou interfaces reativas:

// Retorna um canal de core.Event
events, err := userRepo.Watch(ctx, "users/*")

go func() {
    for event := range events {
        fmt.Printf("Mudança detectada em %s\n", event.ID)
        // Recarregue o documento tipado se necessário
        newUser, _ := userRepo.Get(ctx, event.ID)
    }
}()

📂 Exemplos e Receitas

Demos (Funcionalidades do Core)
Recipes (Casos de Uso)

📚 Documentação Técnica

Tuning de Performance

Se sua aplicação lida com rajadas massivas de eventos (ex: git checkout em repositórios enormes) e você nota que o watcher "congela", pode ser necessário aumentar o buffer de eventos para evitar bloqueios:

// Aumenta o buffer para 1000 eventos (Padrão: 100)
srv, _ := loam.New("path/to/vault", loam.WithEventBuffer(1000))

Known Issues

Linux/inotify
  • Devido a limitações do inotify, novos diretórios criados após o início do watcher não são monitorados automaticamente (é necessário reiniciar o processo ou recriar o watcher). Em Windows e macOS, isso geralmente funciona nativamente.
  • Repositórios muito grandes (milhares de diretórios) podem exceder o limite de file descriptors. Aumente o limite via sysctl fs.inotify.max_user_watches se necessário.
CSV & Nested Data
  • O Loam agora suporta Smart CSV, que detecta estruturas JSON aninhadas (map, []interface{}) e as preserva automaticamente.
  • Caveat (False Positives): Strings que parecem JSON (ex: "{foo}") podem ser interpretadas como objetos se não estiverem escapadas (ex: "\"{foo}\""). Em casos de ambiguidade, o parser favorece a estrutura.
  • Concorrência: A escrita em coleções (CSV) não possui locking de arquivo (flock). O uso concorrente por múltiplos processos pode resultar em perda de dados (Race Condition no ciclo Read-Modify-Write).

Status

🚧 Alpha. A API Go (github.com/aretw0/loam) e a CLI são estáveis para uso diário (Unix Compliant). Novas features como suporte a Coleções JSON/YAML estão em desenvolvimento ativo no Adapter FS.

Licença

AGPL-3.0

Documentation

Overview

Package loam is the Composition Root for the Loam application.

It connects the core business logic (Domain Layer) with the infrastructure adapters (Persistence Layer) using the Hexagonal Architecture pattern.

Philosophy:

Loam is an "Embedded Transactional Engine" for content & metadata. It treats a collection of documents as a transactional database, abstracting the underlying storage mechanism. While the default implementation uses the File System and Git, Loam's core is agnostic, allowing for future adapters (e.g., S3, SQLite).

Features:

  • **Hexagonal Architecture**: Core domain is isolated from persistence details.
  • **Transactional Safe**: Atomic operations regardless of the underlying storage.
  • **Metadata First**: Native support for structured metadata indexing (Frontmatter, JSON fields, etc).
  • **Typed Retrieval**: Generic wrapper (`OpenTypedRepository[T]`) for type-safe document access.
  • **Default Adapter (FS + Git)**: Out-of-the-box support for local Markdown files with Git versioning.
  • **Extensible**: Designed to support other backends (SQL, S3, NoSQL) via `core.Repository`.

Usage:

// Initialize service with functional options
svc, err := loam.New("./vault",
	loam.WithAutoInit(true),
	loam.WithLogger(logger),
)

// Save a document with a change reason (semantics)
ctx := context.WithValue(context.Background(), core.ChangeReasonKey, "initial check-in")
err := svc.SaveDocument(ctx, "my-note", "content", nil)
Example (Basic)

Example_basic demonstrates how to initialize a Vault, save a note, and read it back.

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/aretw0/loam"
	"github.com/aretw0/loam/pkg/core"
)

func main() {
	// Create a temporary directory for the example
	tmpDir, err := os.MkdirTemp("", "loam-example-*")
	if err != nil {
		log.Fatal(err)
	}
	defer os.RemoveAll(tmpDir)

	// Initialize the Loam service (Vault) targeting the temporary directory.
	// WithAutoInit(true) ensures the underlying storage (git repo) is initialized.
	vault, err := loam.New(tmpDir, loam.WithAutoInit(true))
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()

	// 1. Save a Document
	err = vault.SaveDocument(ctx, "hello-world", "This is my first note in Loam.", core.Metadata{
		"tags":   []string{"example"},
		"author": "Gopher",
	})
	if err != nil {
		log.Fatal(err)
	}

	// 2. Read it back
	doc, err := vault.GetDocument(ctx, "hello-world")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Found document: %s\n", doc.ID)
}
Output:
Found document: hello-world
Example (CsvNestedData)

Example_csvNestedData demonstrates Loam's "Smart CSV" capability, which automatically handles nested structures (like maps or slices) by serializing them as JSON within the CSV column.

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"path/filepath"

	"github.com/aretw0/loam"
)

func main() {
	// Setup: Temporary repository
	tmpDir, err := os.MkdirTemp("", "loam-csv-example-*")
	if err != nil {
		log.Fatal(err)
	}
	defer os.RemoveAll(tmpDir)

	repo, err := loam.Init(filepath.Join(tmpDir, "vault"), loam.WithAutoInit(true))
	if err != nil {
		log.Fatal(err)
	}

	type Metrics struct {
		Host string            `json:"host"`
		Tags map[string]string `json:"tags"` // Nested Map
		Load []int             `json:"load"` // Nested Slice
	}

	metricsRepo := loam.NewTypedRepository[Metrics](repo)
	ctx := context.Background()

	// 1. Save complex data to CSV
	err = metricsRepo.Save(ctx, &loam.DocumentModel[Metrics]{
		ID: "metrics/server-01.csv", // .csv extension triggers CSV adapter
		Data: Metrics{
			Host: "server-01",
			Tags: map[string]string{"env": "prod", "region": "us-east"},
			Load: []int{10, 20, 15},
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	// 2. Read it back
	// Loam automatically parses the JSON strings inside the CSV back into Maps and Slices.
	doc, err := metricsRepo.Get(ctx, "metrics/server-01.csv")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("Host: %s\n", doc.Data.Host)
	fmt.Printf("Tag Region: %s\n", doc.Data.Tags["region"])
	fmt.Printf("Load: %v\n", doc.Data.Load)
}
Output:
Host: server-01
Tag Region: us-east
Load: [10 20 15]
Example (CustomSerializer)

Example_customSerializer shows how to inject a custom serializer using built-in factories.

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"path/filepath"

	"github.com/aretw0/loam"
	"github.com/aretw0/loam/pkg/adapters/fs"
)

func main() {
	// Setup
	tmpDir, err := os.MkdirTemp("", "loam-custom-*")
	if err != nil {
		log.Fatal(err)
	}
	defer os.RemoveAll(tmpDir)

	// Initialize with Strict JSON Serializer provided by the FS adapter
	// This replaces the default JSON serializer with one that preserves large integers.
	repo, err := loam.Init(filepath.Join(tmpDir, "vault"),
		loam.WithAutoInit(true),
		loam.WithSerializer(".json", fs.NewJSONSerializer(true)),
	)
	if err != nil {
		log.Fatal(err)
	}

	ctx := context.Background()

	// Save a document with a large number that might lose precision in float64
	jsonContent := `{"big_id": 9223372036854775807, "content": "Large Int"}`
	err = os.WriteFile(filepath.Join(tmpDir, "vault", "strict.json"), []byte(jsonContent), 0644)
	if err != nil {
		log.Fatal(err)
	}

	// Read it back via Loam
	doc, err := repo.Get(ctx, "strict.json")
	if err != nil {
		log.Fatal(err)
	}

	// Check type of big_id
	val := doc.Metadata["big_id"]
	fmt.Printf("Type: %T\n", val)
	fmt.Printf("Value: %v\n", val)

}
Output:
Type: json.Number
Value: 9223372036854775807

Index

Examples

Constants

View Source
const (
	CommitTypeFeat     = platform.CommitTypeFeat
	CommitTypeFix      = platform.CommitTypeFix
	CommitTypeDocs     = platform.CommitTypeDocs
	CommitTypeStyle    = platform.CommitTypeStyle
	CommitTypeRefactor = platform.CommitTypeRefactor
	CommitTypePerf     = platform.CommitTypePerf
	CommitTypeTest     = platform.CommitTypeTest
	CommitTypeChore    = platform.CommitTypeChore
)

Variables

View Source
var Version string

Functions

func AppendFooter added in v0.5.1

func AppendFooter(msg string) string

AppendFooter appends the Loam footer to an arbitrary message.

func FindVaultRoot added in v0.8.1

func FindVaultRoot(startDir string) (string, error)

FindVaultRoot recursively looks upwards for a vault root indicator.

func FormatChangeReason added in v0.5.1

func FormatChangeReason(ctype, scope, subject, body string) string

FormatChangeReason builds a Conventional Commit message.

func Init added in v0.5.1

func Init(path string, opts ...Option) (core.Repository, error)

Init initializes a repository explicitly.

func IsDevRun added in v0.5.1

func IsDevRun() bool

IsDevRun checks if the current process is running via `go run` or `go test`.

func New added in v0.5.1

func New(path string, opts ...Option) (*core.Service, error)

New creates a new Loam Service.

func NewTypedRepository added in v0.8.1

func NewTypedRepository[T any](repo core.Repository) *typed.Repository[T]

NewTypedRepository creates a type-safe wrapper around an existing repository.

Example

ExampleNewTypedRepository demonstrates how to use the Generic Typed Wrapper for type safety.

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"path/filepath"

	"github.com/aretw0/loam"
)

func main() {
	// Setup: Temporary repository
	tmpDir, err := os.MkdirTemp("", "loam-typed-example-*")
	if err != nil {
		log.Fatal(err)
	}
	defer os.RemoveAll(tmpDir)

	// Use loam.Init to get the Repository directly
	repo, err := loam.Init(filepath.Join(tmpDir, "vault"), loam.WithAutoInit(true))
	if err != nil {
		log.Fatal(err)
	}

	// Define your Domain Model
	type User struct {
		Name  string `json:"name"`
		Email string `json:"email"`
	}

	// Wrap the repository
	userRepo := loam.NewTypedRepository[User](repo)
	ctx := context.Background()

	// Save a typed document
	err = userRepo.Save(ctx, &loam.DocumentModel[User]{
		ID:      "users/alice",
		Content: "Alice's Profile",
		Data: User{
			Name:  "Alice",
			Email: "alice@example.com",
		},
	})
	if err != nil {
		log.Fatal(err)
	}

	// Retrieve it back
	doc, err := userRepo.Get(ctx, "users/alice")
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("User Name: %s\n", doc.Data.Name)
}
Output:
User Name: Alice

func NewTypedService added in v0.8.1

func NewTypedService[T any](svc *core.Service) *typed.Service[T]

NewTypedService creates a type-safe wrapper around an existing service.

func OpenTypedRepository added in v0.8.1

func OpenTypedRepository[T any](path string, opts ...Option) (*typed.Repository[T], error)

OpenTypedRepository simplifies creating a TypedRepository from a path.

func OpenTypedService added in v0.8.1

func OpenTypedService[T any](path string, opts ...Option) (*typed.Service[T], error)

OpenTypedService simplifies creating a TypedService from a path.

func ResolveVaultPath added in v0.5.1

func ResolveVaultPath(userPath string, forceTemp bool) string

ResolveVaultPath determines the actual path for the vault based on safety rules.

func Sync added in v0.5.1

func Sync(path string, opts ...Option) error

Sync performs a synchronization (pull/push) of the vault.

Types

type DocumentModel added in v0.6.0

type DocumentModel[T any] = typed.DocumentModel[T]

DocumentModel is a public alias for the typed document model.

type Option added in v0.5.1

type Option = platform.Option

Option defines a functional option for configuring Loam.

func WithAdapter added in v0.5.1

func WithAdapter(name string) Option

WithAdapter allows specifying the storage adapter to use by name.

func WithAutoInit added in v0.5.1

func WithAutoInit(auto bool) Option

WithAutoInit enables automatic initialization of the vault (creates directory and git init).

func WithEventBuffer added in v0.9.0

func WithEventBuffer(size int) Option

func WithForceTemp added in v0.5.1

func WithForceTemp(force bool) Option

WithForceTemp forces the use of a temporary directory (useful for testing).

func WithLogger added in v0.5.1

func WithLogger(logger *slog.Logger) Option

WithLogger sets the logger for the service.

func WithMustExist added in v0.5.1

func WithMustExist(must bool) Option

WithMustExist ensures the vault directory must already exist.

func WithRepository added in v0.5.1

func WithRepository(repo core.Repository) Option

WithRepository allows injecting a custom storage adapter.

func WithSerializer added in v0.10.2

func WithSerializer(ext string, s any) Option

WithSerializer registers a custom serializer for a specific extension. The serializer must implement the adapter's Serializer interface.

func WithSystemDir added in v0.6.0

func WithSystemDir(name string) Option

WithSystemDir allows specifying the hidden directory name (e.g. ".loam").

func WithVersioning added in v0.5.1

func WithVersioning(enabled bool) Option

WithVersioning enables or disables version control (e.g. Git).

type TypedRepository added in v0.6.0

type TypedRepository[T any] = typed.Repository[T]

TypedRepository is a public alias for the typed repository.

type TypedService added in v0.8.1

type TypedService[T any] = typed.Service[T]

TypedService is a public alias for the typed service.

Directories

Path Synopsis
cmd
loam command
examples
internal
pkg
core
Document is the central entity of the domain.
Document is the central entity of the domain.
git

Jump to

Keyboard shortcuts

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