Skip to content

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

License

Notifications You must be signed in to change notification settings

doomspork/cobblestone

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

30 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Cobblestone

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

Syntax Description Example
. Identity (returns input) . => entire structure
.key Access map key .user.name
..key Recursive search ..author finds all authors
.key1.key2 Nested access .store.book

Array Operations

Syntax Description Example
[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

Syntax Description Example
[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

Function Description Example
select(expr) Filter elements select(.active)
map(expr) Transform elements map(.title)

Construction

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

Pipeline

Syntax Description Example
expr | expr Chain 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 Case XPath JSONPath jq Cobblestone
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 filters N/A $.store.book[?(@.isbn)][?(@.price<10)] .store.book[] | select(.isbn) | select(.price < 10) .store.book | [isbn] | [price<10]
Extract titles N/A $.store.book[*].title .store.book[].title .store.book | map(.title)
Filter by field N/A $.store.book[?(@.isbn)] .store.book[] | select(.isbn) .store.book[] | select(.isbn)
Transform to object N/A N/A {title: .title, cost: .price} {title: .title, cost: .price}
Array construction N/A N/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

About

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

Topics

Resources

License

Stars

Watchers

Forks

Contributors 2

  •  
  •