pathlang

package module
v0.0.0-...-c40c2f6 Latest Latest
Warning

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

Go to latest
Published: Jan 18, 2026 License: Unlicense Imports: 5 Imported by: 0

README

pathlang

A domain-agnostic path query language for expressing hierarchical resource locators with predicates, inspired by XPath but designed for simplicity and ease of use.

Overview

Pathlang provides a simple, expressive syntax for selecting resources in hierarchical structures. It's designed to be:

  • Domain agnostic: Works with any hierarchical data through a resolver interface
  • Easy to type: Shell-friendly syntax without excessive quoting
  • Simple but extensible: Small core syntax with room for future growth
  • Separate from actions: Pure selection language (actions like @close live outside this library)

Syntax

Basic Examples
/                                                # Root
/projects                                        # All projects
/projects[name=foo]                             # Projects where name equals "foo"
/projects[name="foo bar"]                       # Quoted values for spaces
/projects[name=foo]/tasks[status=open]          # Nested path
/tasks[status=open,assignee=me]                 # Multiple predicates (AND)
/tasks[description~=bug]                        # Pattern matching
Grammar

Paths are always absolute (start with /) and consist of segments separated by /:

Path      ::= "/" Segments?
Segments  ::= Segment ( "/" Segment )*
Segment   ::= Name Predicates?
Predicates ::= "[" PredicateList "]"
PredicateList ::= Predicate ( "," Predicate )*
Predicate ::= FieldName Op Value
Identifiers

Identifiers (segment names and field names) can contain:

  • Letters (a-z, A-Z)
  • Digits (0-9, but not at the start)
  • Underscores (_)
  • Hyphens (-)

Examples: projects, task_items, my-field-name

Operators
Operator Meaning Example
= Equality status=open
!= Inequality status!=closed
~= Pattern match description~=bug

Pattern match semantics (typically substring or glob) are defined by the resolver.

Values

Values can be bare or quoted:

Bare values can contain any characters except:

  • Whitespace
  • /, [, ], ,, =, !, ~, @, ", '

Quoted values use double quotes with escape sequences:

  • \" - literal quote
  • \\ - literal backslash
  • \n - newline
  • \t - tab

Examples:

name=foo              # Bare value
name="foo bar"        # Quoted value with space
path="C:\\Users"      # Escaped backslash
text="line1\nline2"   # Escaped newline
Predicates

All predicates in a segment are implicitly ANDed together:

/tasks[status=open,assignee=me]   # status=open AND assignee=me

API

Parsing
// Parse a path string
path, err := pathlang.Parse("/projects[name=foo]/tasks")
if err != nil {
    // handle error
}

// Convert back to canonical string form
fmt.Println(path.String())  // "/projects[name=foo]/tasks"
Types
type Path struct {
    Segments []Segment
}

type Segment struct {
    Name       string
    Predicates []Predicate
}

type Predicate struct {
    Field string
    Op    Op        // OpEq, OpNotEq, OpMatch
    Value string    // Always a string lexically
}
Evaluation

To evaluate paths against your domain data, implement the Resolver interface:

type Resolver interface {
    // Root returns the logical root node
    Root(ctx context.Context) (Node, error)

    // Children resolves a segment under a parent node
    // Should:
    //   - Filter by segment.Name
    //   - Apply all predicates
    //   - Return all matching child nodes
    Children(ctx context.Context, parent Node, seg Segment) ([]Node, error)
}

Then evaluate paths:

// Evaluate from root
nodes, err := pathlang.Eval(ctx, resolver, path)

// Or evaluate from a specific node
nodes, err := pathlang.EvalFrom(ctx, resolver, startNode, path)
Implementing a Resolver

Here's a simple example:

type MyResolver struct {
    // your domain data
}

func (r *MyResolver) Root(ctx context.Context) (pathlang.Node, error) {
    return r.rootObject, nil
}

func (r *MyResolver) Children(ctx context.Context, parent pathlang.Node, seg pathlang.Segment) ([]pathlang.Node, error) {
    obj := parent.(*MyObject)

    var matches []pathlang.Node
    for _, child := range obj.Children {
        // Check if child's type matches segment name
        if child.Type() != seg.Name {
            continue
        }

        // Apply predicates
        if !r.matchesPredicates(child, seg.Predicates) {
            continue
        }

        matches = append(matches, child)
    }

    return matches, nil
}

func (r *MyResolver) matchesPredicates(obj *MyObject, preds []pathlang.Predicate) bool {
    for _, pred := range preds {
        value := obj.GetField(pred.Field)
        switch pred.Op {
        case pathlang.OpEq:
            if value != pred.Value {
                return false
            }
        case pathlang.OpNotEq:
            if value == pred.Value {
                return false
            }
        case pathlang.OpMatch:
            if !strings.Contains(strings.ToLower(value), strings.ToLower(pred.Value)) {
                return false
            }
        }
    }
    return true
}

Semantics

Evaluation is set-based:

  1. Start with a singleton set containing the root node
  2. For each segment:
    • For each node in the current set, get matching children
    • Union all children to form the next set
  3. Return the final set

This means paths like /projects/tasks will:

  1. Start with {root}
  2. Get all projects children → {project1, project2, ...}
  3. Get all tasks children from each project → {task1, task2, task3, ...}

Design Principles

What's Included (v1)

✅ Absolute paths with hierarchical segments ✅ Simple predicates with AND conjunction ✅ Three operators: =, !=, ~= ✅ Quoted and unquoted values ✅ Domain-agnostic resolver interface

Reserved for Future

These are intentionally not included in v1 but the syntax reserves room for them:

  • Boolean operators (and, or) in predicates
  • More comparison operators (>, <, >=, <=, in)
  • Descendant operator (//tasks[status=open])
  • Projection/field selection
  • Update semantics

The syntax is designed so these extensions won't break existing queries.

Integration Examples

With tk (task manager)
# Pure selection (what pathlang handles)
tk /projects[name=foo]/tasks[status=open]

# With actions (outside pathlang)
tk /tasks[id=43] @close

Pathlang only handles the path part; @close would be parsed separately by the CLI.

Custom Application
// Parse user input
path, err := pathlang.Parse(userInput)

// Evaluate against your domain
resolver := &MyDomainResolver{...}
results, err := pathlang.Eval(ctx, resolver, path)

// Process results
for _, node := range results {
    fmt.Printf("Found: %v\n", node)
}

Error Handling

Parse errors include position information:

path, err := pathlang.Parse("/projects[name=")
// Error: parse error at position 15: expected value
//   context: "[name="
//   position:       ^

Testing

Run tests:

cd lib/pathlang
go test -v

The test suite includes:

  • Parser tests with valid and invalid inputs
  • Round-trip tests (parse → string → parse)
  • Evaluation tests with a mock resolver
  • Error handling tests

License

See the LICENSE file in the repository root.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Node

type Node any

Node represents a domain object in the resolver's graph. It is opaque to pathlang; the resolver interprets it.

func Eval

func Eval(ctx context.Context, r Resolver, p *Path) ([]Node, error)

Eval evaluates a path from the resolver's root. Returns all nodes matching the path, or an error.

func EvalFrom

func EvalFrom(ctx context.Context, r Resolver, start Node, p *Path) ([]Node, error)

EvalFrom evaluates a path from a specific starting node. This is useful for evaluating relative paths or sub-paths.

type Op

type Op int

Op represents a predicate comparison operator.

const (
	// OpEq represents the equality operator (=).
	OpEq Op = iota
	// OpNotEq represents the inequality operator (!=).
	OpNotEq
	// OpMatch represents the pattern match operator (~=).
	// Semantics are defined by the resolver (typically contains or glob).
	OpMatch
)

func (Op) String

func (o Op) String() string

String returns the string representation of an operator.

type Parser

type Parser struct {
	// contains filtered or unexported fields
}

Parser is a parser for the pathlang language.

func NewParser

func NewParser() (*Parser, error)

NewParser creates a new parser for pathlang.

func (*Parser) MustParse

func (p *Parser) MustParse(input string) *Path

MustParse parses an input string and panics on error. This is useful for testing.

func (*Parser) Parse

func (p *Parser) Parse(input string) (*Path, error)

Parse parses an input string into a Path.

type Path

type Path struct {
	// Segments are the path components.
	// Empty for root path (/).
	Segments []Segment
	// Action is the optional action name (e.g., "add" from "@add").
	// Empty string means no action specified.
	Action string
	// ActionArgs are the arguments for the action.
	// Empty when no action or no arguments provided.
	ActionArgs []string
}

Path represents a complete path expression. Paths are always absolute (start with /).

func MustParse

func MustParse(input string) *Path

MustParse is a convenience function that creates a new parser and parses the input, panicking on error.

func Parse

func Parse(input string) (*Path, error)

Parse is a convenience function that creates a new parser and parses the input.

func (*Path) String

func (p *Path) String() string

String returns the canonical string representation of a path.

type Predicate

type Predicate struct {
	// Field is the name of the field to test.
	Field string
	// Op is the comparison operator.
	Op Op
	// Value is the value to compare against (always a string lexically).
	Value string
}

Predicate represents a single field comparison within a segment. All predicates in a segment are implicitly ANDed together.

func (Predicate) String

func (p Predicate) String() string

String returns the canonical string representation of a predicate.

type Resolver

type Resolver interface {
	// Root returns the logical root node for path resolution.
	Root(ctx context.Context) (Node, error)

	// Children resolves a segment under a parent node.
	// It should:
	//   - Filter children by segment.Name
	//   - Apply all predicates (implicit AND)
	//   - Return all matching child nodes
	Children(ctx context.Context, parent Node, seg Segment) ([]Node, error)
}

Resolver defines how to navigate a domain-specific node graph. Implementations map path segments to actual domain objects.

type Segment

type Segment struct {
	// Name is the segment name (e.g., "projects", "tasks").
	Name string
	// Predicates are the filters applied at this segment.
	// All predicates must be satisfied (implicit AND).
	Predicates []Predicate
}

Segment represents a single path segment with optional predicates.

func (Segment) String

func (s Segment) String() string

String returns the canonical string representation of a segment.

Jump to

Keyboard shortcuts

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