cirru_edn 0.8.0

Parser/Writer for Cirru EDN
Documentation

Cirru Edn in Rust

Extensible data notations based on Cirru syntax

Usages

Rust Docs.

cargo add cirru_edn

Basic parsing and formatting:

use cirru_edn::Edn;

cirru_edn::parse("[] 1 2 true"); // Result<Edn, String>

cirru_edn::format(data, /* use_inline */ true); // Result<String, String>.

Serde Integration

Cirru EDN provides seamless integration with serde, allowing you to easily convert between Rust structs and EDN data with efficient direct serialization and deserialization.

Key Type Distinction

An important feature of this implementation is the semantic distinction between struct fields and map keys:

  • Struct fields use Tag (:field_name) - representing named constants and structured identifiers
  • Map keys use String ("key") - representing arbitrary string data

This design preserves the intended meaning of different data elements in EDN format.

Basic Usage

use cirru_edn::{to_edn, from_edn};
use serde::{Serialize, Deserialize};
use std::collections::HashMap;

#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct Person {
    name: String,
    age: u32,
    email: Option<String>,
    tags: Vec<String>,
    metadata: HashMap<String, String>,  // Map keys will be Strings
}

let person = Person {
    name: "Alice".to_string(),
    age: 30,
    email: Some("alice@example.com".to_string()),
    tags: vec!["developer".to_string(), "rust".to_string()],
    metadata: [("role".to_string(), "senior".to_string())].into_iter().collect(),
};

// Convert struct to Edn
let edn_value = to_edn(&person).unwrap();
println!("EDN: {}", edn_value);
// Output: {:name "Alice", :age 30, :email "alice@example.com", :tags ["developer", "rust"], :metadata {"role" "senior"}}
//          ^^^^^ Tag                                                                                    ^^^^^^ String

// Convert Edn back to struct
let reconstructed: Person = from_edn(edn_value).unwrap();
assert_eq!(person, reconstructed);

Supported Data Types

  • Primitive types: bool, i32, i64, u32, u64, f32, f64, String
  • Container types: Vec<T>, HashMap<K, V>, HashSet<T>
  • Optional types: Option<T> (maps to Edn::Nil or the actual value)
  • Nested structures: Arbitrarily deep nested structs

Manual Edn Construction

You can also manually construct Edn data and then deserialize it to structs. Remember to use Tags for struct field keys:

use cirru_edn::{Edn, EdnTag, EdnMapView, from_edn};
use std::collections::HashMap;

// Construct EDN manually with proper key types
let mut map = HashMap::new();
map.insert(Edn::Tag(EdnTag::new("name")), "Bob".into());         // Tag for struct field
map.insert(Edn::Tag(EdnTag::new("age")), Edn::Number(25.0));     // Tag for struct field
map.insert(Edn::Tag(EdnTag::new("email")), Edn::Nil);            // Tag for struct field
map.insert(Edn::Tag(EdnTag::new("tags")), vec!["junior".to_string(), "javascript".to_string()].into());

// For metadata HashMap, use String keys
let mut metadata_map = HashMap::new();
metadata_map.insert(Edn::Str("department".into()), Edn::Str("engineering".into()));  // String for map key
map.insert(Edn::Tag(EdnTag::new("metadata")), Edn::Map(EdnMapView(metadata_map)));

let edn_data = Edn::Map(EdnMapView(map));
let person: Person = from_edn(edn_data).unwrap();
println!("{:?}", person);

Error Handling

When deserialization fails (e.g., missing required fields or type mismatches), descriptive error messages are returned:

let incomplete_edn = Edn::map_from_iter([
    ("name".into(), "Invalid".into()),
    // Missing required age field
]);

match from_edn::<Person>(incomplete_edn) {
    Ok(person) => println!("Success: {:?}", person),
    Err(e) => println!("Error: {}", e), // Error: missing field `age`
}

Complex Examples

See examples/serde_demo.rs for more complex nested structures and usage patterns.

Struct Deserialization

Cirru EDN supports Struct values with symbol names, which can be deserialized to Rust structs. During deserialization, the struct name is ignored since Rust structs don't expose their type names at runtime:

use cirru_edn::{Edn, EdnStructView, EdnTag, from_edn};
use std::sync::Arc;

// Create a Struct with a symbol name
let person_struct = Edn::Struct(EdnStructView {
    name: Arc::from("Person"),  // This name will be ignored during deserialization
    pairs: vec![
        (EdnTag::new("name"), "Frank".into()),
        (EdnTag::new("age"), Edn::Number(42.0)),
        (EdnTag::new("email"), "frank@example.com".into()),
    ],
});

// Deserialize Struct to Rust struct (ignoring the struct name)
let person: Person = from_edn(person_struct).unwrap();
println!("{:?}", person);

// Note: When serializing structs back to EDN, they become Maps, not EDN Struct values
// since Rust doesn't provide struct names at runtime
let edn_back = to_edn(&person).unwrap();
// This will be a Map, not an EDN Struct

This feature allows interoperability between EDN data containing Struct values and Rust structs, with the semantic understanding that struct names are metadata that may be lost during round-trip conversion.

Limitations

  • Some special Edn types (like Quote, AnyRef) cannot be serialized
  • Maps with complex keys will use their string representation when serializing structs
  • Struct names are ignored during deserialization and Rust structs serialize to Maps, not EDN Struct values

EDN Format

mixed data:

{} (:a 1.0)
  :b $ [] 2.0 3.0 4.0
  :c $ {} (:d 4.0)
    :e true
    :f :g
    :h $ {} (|a 1.0)
      |b true
{}
  :b $ [] 2 3 4
  :a 1
  :c $ {}
    :h $ {} (|b true) (|a 1)
    :f :g
    :e true
    :d 4

for top-level literals, need to use do expression:

do nil
do true
do false
do 1
do -1.1

quoted code:

do 'a
quote (a b)

tags(previously called "keyword")

do :a

string syntax, note it's using prefixed syntax of |:

do |a

string with special characters:

do \"|a b\"

nested list:

[] 1 2 $ [] 3
#{} ([] 3) 1

enum, or tagged union, is limited by Calcit semantics:

:: 'a

:: 'b 1

extra values can be added to an enum:

:: 'a 1 |extra :l

%:: represents enums with a symbol type name:

%:: 'e 'a 1 |extra :l

an EDN struct uses a symbol name and tag field keys:

%{} 'Demo (:a 1)
  :b 2
  :c $ [] 1 2 3

extra format for holding buffer, which is internally Vec<u8>:

buf 00 01 f1 11

atom, which translates to a reference to a value:

atom 1

Error Handling (v0.7.0+)

With cirru_parser 0.2.0, Cirru EDN provides enhanced syntax-error reporting with source position information:

use cirru_edn::{parse, EdnError};

match parse("[] 1 2 invalid") {
    Ok(data) => println!("Parsed: {:?}", data),
    Err(EdnError::ParseError { original }) => println!("Error: {}", original),
    Err(e) => println!("Other error: {}", e),
}

Error types include:

  • ParseError - Syntax errors from the parser
  • StructureError - Invalid EDN structure
  • ValueError - Invalid values (e.g., bad hex, invalid tokens)
  • DeserializationError - Serde deserialization errors

Structure and value errors include a nested Cirru path in @1.2.3 notation; for example, @1.2.3.4 means indices 1, 2, 3, and 4 from the root node. Syntax errors include line/column and byte-offset information when provided by the parser.

See ERROR_HANDLING.md for detailed documentation and examples.

Run the error demo:

cargo run --example error_demo

License

MIT