Cobblestone

View Source

A better path to data. Powerful data querying and transformation library for Elixir

Continuous Integration Module Version Hex Docs Total Download License Last Updated

Cobblestone provides a path-based query language for navigating and filtering nested maps and lists in Elixir, inspired by jq, JSONPath, and XPath. It offers a simple yet powerful syntax for extracting, transforming, and manipulating data structures.

Features

  • Path Navigation: Direct access (.store.book) and recursive search (..author)
  • Array Operations: Indexing ([0], [-1]), slicing ([1:3]), multiple indices ([0,2,4])
  • Filtering: Existence filters ([isbn]) and comparison filters ([price>20])
  • Data Transformation: map() for transforming arrays, select() for filtering
  • Pipeline Operations: Chain operations with the pipe operator (|)
  • Object/Array Construction: Build new structures with {key: .path} and [.path1, .path2]
  • Elixir Integration: Supports both atom and string keys, pipeline-friendly APIs
  • Error Handling: Structured error responses with helpful messages

Installation

Add cobblestone to your list of dependencies in mix.exs:

def deps do
  [
    {:cobblestone, "~> 0.1.0"}
  ]
end

Quick Start

# Basic path navigation
data = %{"user" => %{"name" => "Alice", "age" => 30}}
Cobblestone.get_at_path!(data, ".user.name")
# => "Alice"

# Array operations
books = %{"items" => [%{"title" => "Elixir in Action", "price" => 35}]}
Cobblestone.get_at_path!(books, ".items[0].title")
# => ["Elixir in Action"]

# Pipeline operations
users = [
  %{"name" => "Alice", "active" => true, "age" => 30},
  %{"name" => "Bob", "active" => false, "age" => 25}
]
Cobblestone.get_at_path!(users, ".[] | select(.active) | map(.name)")
# => ["Alice"]

# Object construction
user = %{"first" => "Alice", "last" => "Smith", "role" => "admin"}
Cobblestone.get_at_path!(user, "{name: .first, position: .role}")
# => %{"name" => "Alice", "position" => "admin"}

Path Syntax Guide

Basic Navigation

SyntaxDescriptionExample
.Identity (returns input). => entire structure
.keyAccess map key.user.name
..keyRecursive search..author finds all authors
.key1.key2Nested access.store.book

Array Operations

SyntaxDescriptionExample
[n]Index access[0] first, [-1] last
[n:m]Slice range[1:3] elements 1-2
[:m]Slice from start[:3] first 3 elements
[n:]Slice to end[2:] from index 2 onward
[n,m,o]Multiple indices[0,2,4] specific elements
[]Array/object iterator.items[] all array elements

Filtering

SyntaxDescriptionExample
[key]Has key[isbn] items with isbn field
[key>value]Greater than[price>20]
[key<value]Less than[price<10]
[key>=value]Greater or equal[age>=18]
[key<=value]Less or equal[stock<=5]
[key==value]Equals[status=="active"]

Functions

FunctionDescriptionExample
select(expr)Filter elementsselect(.active)
map(expr)Transform elementsmap(.title)

Construction

SyntaxDescriptionExample
{key: expr}Object construction{name: .first, age: .age}
[expr, expr]Array construction[.name, .email, .phone]

Pipeline

SyntaxDescriptionExample
expr | exprChain operations.users | select(.active) | map(.name)

API Reference

Core Functions

get_at_path(data, path)

Query data using path expressions, returning {:ok, result} or {:error, details}.

data = %{"users" => [%{"name" => "Alice"}, %{"name" => "Bob"}]}
{:ok, names} = Cobblestone.get_at_path(data, ".users[].name")
# => {:ok, ["Alice", "Bob"]}

get_at_path!(data, path)

Like get_at_path/2 but returns the result directly or raises on error.

Cobblestone.get_at_path!(data, ".users[].name")
# => ["Alice", "Bob"]

Functional API

at(path)

Creates a reusable query function for the given path expression.

get_names = Cobblestone.at(".users[].name")
{:ok, names} = get_names.(data)

at!(path)

Creates a reusable query function that raises on error.

get_names = Cobblestone.at!(".users[].name")
names = get_names.(data)  # Raises on error

Extraction Functions

extract(data, path_map)

Extract multiple values from data using a map of path expressions.

paths = %{
  user_name: ".user.name",
  user_age: ".user.age",
  settings: ".user.settings"
}
{:ok, extracted} = Cobblestone.extract(data, paths)

extract!(data, path_map)

Like extract/2 but raises on error.

extracted = Cobblestone.extract!(data, paths)

Advanced Examples

Complex Data Extraction

api_response = %{
  "data" => %{
    "users" => [
      %{"id" => 1, "name" => "Alice", "posts" => [%{"title" => "Hello"}]},
      %{"id" => 2, "name" => "Bob", "posts" => [%{"title" => "World"}]}
    ]
  },
  "meta" => %{"total" => 2}
}

# Extract multiple fields
Cobblestone.extract!(api_response, %{
  names: ".data.users[].name",
  total: ".meta.total",
  all_posts: ".data.users[].posts[].title"
})
# => %{names: ["Alice", "Bob"], total: 2, all_posts: ["Hello", "World"]}

Data Transformation Pipeline

products = [
  %{"name" => "Laptop", "price" => 999, "stock" => 5, "active" => true},
  %{"name" => "Mouse", "price" => 25, "stock" => 0, "active" => true},
  %{"name" => "Keyboard", "price" => 75, "stock" => 10, "active" => false}
]

# Complex pipeline: active products in stock, sorted by price
products
|> Cobblestone.get_at_path!(".[] | select(.active)")
|> Cobblestone.get_at_path!(".[stock>0]")
|> Cobblestone.get_at_path!("{name: .name, price: .price}")
# => %{"name" => "Laptop", "price" => 999}

Working with Atom Keys

# Cobblestone seamlessly handles atom keys
config = %{
  database: %{
    host: "localhost",
    port: 5432,
    credentials: %{
      username: "admin",
      password: "secret"
    }
  }
}

Cobblestone.get_at_path!(config, ".database.credentials.username")
# => "admin"

Comparison with Similar Tools

Sample data structure used for comparison:

%{
  "store" => %{
    "book" => [
      %{
        "category" => "reference",
        "author" => "Nigel Rees",
        "title" => "Sayings of the Century",
        "price" => 8.95
      },
      %{
        "category" => "fiction",
        "author" => "Evelyn Waugh",
        "title" => "Sword of Honour",
        "price" => 12.99
      },
      %{
        "category" => "fiction",
        "author" => "Herman Melville",
        "title" => "Moby Dick",
        "isbn" => "0-553-21311-3",
        "price" => 8.99
      },
      %{
        "category" => "fiction",
        "author" => "J. R. R. Tolkien",
        "title" => "The Lord of the Rings",
        "isbn" => "0-395-19395-8",
        "price" => 22.99
      }
    ],
    "bicycle" => %{
      "color" => "red",
      "price" => 19.95
    }
  }
}

Query Syntax Comparison

Use CaseXPathJSONPathjqCobblestone
All book authors/store/book/author$.store.book[*].author.store.book[].author.store.book[].author
All authors (recursive)//author$..author.. | .author? // empty..author
All store items/store/*$.store.*.store[].store[]
All prices in store/store//price$.store..price.store | .. | .price? // empty.store..price
Third book//book[3]$..book[2].store.book[2]..book[2]
Last book//book[last()]$..book[-1:].store.book[-1]..book[-1]
First two books//book[position()<3]$..book[:2].store.book[:2]..book[:2]
Books with ISBN//book[isbn]$..book[?(@.isbn)].store.book[] | select(.isbn)..book[isbn]
Books under $10//book[price<10]$..book[?(@.price<10)].store.book[] | select(.price < 10)..book[price<10]
Books over $20//book[price>20]$..book[?(@.price>20)].store.book[] | select(.price > 20)..book[price>20]
All elements//*$..*...
Chain filtersN/A$.store.book[?(@.isbn)][?(@.price<10)].store.book[] | select(.isbn) | select(.price < 10).store.book | [isbn] | [price<10]
Extract titlesN/A$.store.book[*].title.store.book[].title.store.book | map(.title)
Filter by fieldN/A$.store.book[?(@.isbn)].store.book[] | select(.isbn).store.book[] | select(.isbn)
Transform to objectN/AN/A{title: .title, cost: .price}{title: .title, cost: .price}
Array constructionN/AN/A[.name, .age, .email][.name, .age, .email]

Key Differences

Cobblestone vs JSONPath:

  • Cleaner, more intuitive syntax
  • Native Elixir integration with atoms and strings
  • Built-in pipeline operations
  • Object/array construction support

Cobblestone vs XPath:

  • JSON/Map-oriented rather than XML-focused
  • Simpler array operations
  • Modern functional operations (map, select)

Cobblestone vs jq:

  • Native Elixir library (no external dependencies)
  • Seamless integration with Elixir pipelines
  • Simplified syntax for common operations
  • Type-safe with Elixir's pattern matching

Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgments

  • Inspired by jq, JSONPath, and XPath
  • Built with Erlang's leex and yecc parser generators
  • Designed for the Elixir community