mirror of
https://github.com/linabutler/ploidy
synced 2026-08-11 09:28:38 +00:00
chore(pointer): Clean up README, tests.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ploidy-pointer-derive"
|
||||
description = "Derive macros for Ploidy JSON pointers"
|
||||
description = "Derive macros for Ploidy JSON Pointers"
|
||||
readme = "README.md"
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../LICENSE
|
||||
@@ -1,5 +1,5 @@
|
||||
# ploidy-pointer-derive
|
||||
|
||||
This crate provides a derive macro for automatically implementing the `JsonPointee` trait from the `ploidy-pointer` crate.
|
||||
This crate provides a derive macro for automatically implementing the `JsonPointee` trait from the **ploidy-pointer** crate. For more details about how the macro works, and how to customize the derived implementation, please [see the crate docs](https://docs.rs/ploidy-pointer-derive).
|
||||
|
||||
**Note:** You typically don't need to depend on this crate directly. Instead, please use the `ploidy-pointer` crate with the `derive` feature (enabled by default).
|
||||
**Note:** You typically don't need to depend on this crate directly. Instead, please use **ploidy-pointer**, and enable its `derive` feature.
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../LICENSE
|
||||
+38
-25
@@ -1,17 +1,14 @@
|
||||
# ploidy-pointer
|
||||
|
||||
This crate provides a way to traverse strongly-typed data structures using JSON Pointers ([RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901)). It's part of the [Ploidy](https://crates.io/crates/ploidy) OpenAPI code generator, but can be used standalone.
|
||||
This crate provides a way to traverse typed Rust data structures using JSON Pointers ([RFC 6901](https://datatracker.ietf.org/doc/html/rfc6901)). At its heart is the `JsonPointee` trait, which can be implemented on types to make them traversable.
|
||||
|
||||
The cornerstone of **ploidy-pointer** is the `JsonPointee` trait, which can be implemented on types to make them traversable with JSON Pointers.
|
||||
**ploidy-pointer** is part of the [Ploidy](https://crates.io/crates/ploidy) OpenAPI code generator, but can be used standalone.
|
||||
|
||||
## Features
|
||||
|
||||
- Parse and validate JSON Pointer strings.
|
||||
- Recursively resolve pointers against Rust data structures.
|
||||
- Built-in implementations for primitive and collection types.
|
||||
- Optional support for `serde_json`, `chrono`, `url`, and `indexmap`.
|
||||
- Error handling with helpful suggestions for typos.
|
||||
- Derive macro support via the `derive` feature (enabled by default).
|
||||
- Parse and resolve JSON Pointer strings.
|
||||
- Built-in `JsonPointee` implementations for primitives, collections, and common external types.
|
||||
- Derive `JsonPointee` implementations for your own types.
|
||||
|
||||
### Cargo features
|
||||
|
||||
@@ -24,41 +21,42 @@ The cornerstone of **ploidy-pointer** is the `JsonPointee` trait, which can be i
|
||||
|
||||
## JSON Pointer Syntax
|
||||
|
||||
JSON Pointers are strings that identify a specific value within a JSON document:
|
||||
JSON Pointers are strings that identify a specific value within a JSON structure:
|
||||
|
||||
* `` (empty string) - References the root value.
|
||||
* `/foo` - References the `foo` field.
|
||||
* `/foo/0` - References the first element of the `foo` array.
|
||||
* `/foo/bar` - References the `bar` field of the `foo` object.
|
||||
- `""` (empty string) - References the root value.
|
||||
- `"/foo"` - References the `foo` field.
|
||||
- `"/foo/0"` - References the first element of the `foo` array.
|
||||
- `"/foo/bar"` - References the `bar` field of the `foo` object.
|
||||
|
||||
Two special characters are escaped:
|
||||
Two special characters need to be escaped: `~` is written as `~0`, and `/` is written as `~1`.
|
||||
|
||||
- `~0` represents `~`, and...
|
||||
- `~1` represents `/`.
|
||||
Note that `"/"` (a single slash) does _not_ reference the root; it references a _field_ named `""` (the empty string). If you see an "unknown key" error for a field that you know exists, double-check that an extra slash hasn't snuck in to the pointer string.
|
||||
|
||||
## Usage
|
||||
|
||||
```rust
|
||||
use ploidy_pointer::{JsonPointer, JsonPointee};
|
||||
use ploidy_pointer::{JsonPointee, JsonPointer};
|
||||
use std::collections::HashMap;
|
||||
|
||||
let mut data = HashMap::new();
|
||||
data.insert("foo".to_string(), vec![1, 2, 3]);
|
||||
data.insert("foo".to_owned(), vec![1, 2, 3]);
|
||||
|
||||
// Parse a JSON Pointer
|
||||
// Parse a JSON Pointer.
|
||||
let pointer = JsonPointer::parse("/foo/1").unwrap();
|
||||
|
||||
// Resolve it against your data
|
||||
// Resolve it against your data.
|
||||
let result = data.resolve(pointer).unwrap();
|
||||
|
||||
// Downcast to the expected type
|
||||
// Downcast to the expected type.
|
||||
assert_eq!(result.downcast_ref::<i32>(), Some(&2));
|
||||
```
|
||||
|
||||
### With the derive macro
|
||||
### Deriving `JsonPointee` for your own types
|
||||
|
||||
The `#[derive(JsonPointee)]` macro can generate implementations of `JsonPointer` for structs and enums, and supports [Serde](https://serde.rs)-like attributes for customizing the implementations. For more details, please see the [**ploidy-pointer-derive** docs](https://docs.rs/ploidy-pointer-derive).
|
||||
|
||||
```rust
|
||||
use ploidy_pointer::{JsonPointer, JsonPointee};
|
||||
use ploidy_pointer::{JsonPointee, JsonPointer};
|
||||
|
||||
#[derive(JsonPointee)]
|
||||
struct User {
|
||||
@@ -67,13 +65,13 @@ struct User {
|
||||
}
|
||||
|
||||
let user = User {
|
||||
name: "Alice".to_string(),
|
||||
name: "Alice".to_owned(),
|
||||
age: 30,
|
||||
};
|
||||
|
||||
let pointer = JsonPointer::parse("/name").unwrap();
|
||||
let result = user.resolve(pointer).unwrap();
|
||||
assert_eq!(result.downcast_ref::<String>(), Some(&"Alice".to_string()));
|
||||
assert_eq!(result.downcast_ref::<String>(), Some(&"Alice".to_owned()));
|
||||
```
|
||||
|
||||
### Errors
|
||||
@@ -91,3 +89,18 @@ match user.resolve(pointer) {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Similar crates
|
||||
|
||||
There are many great options for working with JSON Pointers in Rust: [**jsonptr**](https://crates.io/crates/jsonptr), [**json-pointer**](https://crates.io/crates/json-pointer) and its [forks](https://crates.io/crates/json-pointer-simd), and [`serde_json::Value::pointer`](https://docs.rs/serde_json/latest/serde_json/enum.Value.html#method.pointer).
|
||||
|
||||
For native Rust data structures, [**bevy_reflect**](https://crates.io/crates/bevy_reflect) and [**facet**](https://facet.rs) offer much more powerful runtime reflection capabilities.
|
||||
|
||||
**ploidy-pointer** fills a niche somewhere in between these two, providing JSON Pointers for native Rust data structures. This is especially useful for code generators like Ploidy, and strongly-typed API clients that want to navigate structured responses.
|
||||
|
||||
In short:
|
||||
|
||||
- If you're working with structured data, and want to add type-safe JSON Pointer traversal, **ploidy-pointer** could be a good fit.
|
||||
- If you're working with dynamic JSON documents, and want to read and write values, consider **jsonptr** or **json-pointer**.
|
||||
- If you're working with simpler JSON values, and don't need more advanced features, the `pointer()` method on `serde_json::Value` might be enough.
|
||||
- If you'd like full runtime reflection for your structured data, give **bevy_reflect** or **facet** a try.
|
||||
|
||||
@@ -17,7 +17,7 @@ fn test_rename_field() {
|
||||
let result = s.resolve(pointer).unwrap();
|
||||
assert_eq!(result.downcast_ref::<String>(), Some(&"hello".to_owned()));
|
||||
|
||||
// Original name should not work.
|
||||
// Original name should fail.
|
||||
let pointer = JsonPointer::parse("/my_field").unwrap();
|
||||
assert!(s.resolve(pointer).is_err());
|
||||
}
|
||||
@@ -67,7 +67,7 @@ fn test_rename_all_camel_case() {
|
||||
let result = s.resolve(pointer).unwrap();
|
||||
assert_eq!(result.downcast_ref::<i32>(), Some(&42));
|
||||
|
||||
// Original snake_case should not work.
|
||||
// Original snake_case should fail.
|
||||
let pointer = JsonPointer::parse("/my_field").unwrap();
|
||||
assert!(s.resolve(pointer).is_err());
|
||||
}
|
||||
@@ -165,7 +165,7 @@ fn test_enum_with_rename() {
|
||||
let result = e.resolve(pointer).unwrap();
|
||||
assert_eq!(result.downcast_ref::<String>(), Some(&"hello".to_owned()));
|
||||
|
||||
// Original name should not work.
|
||||
// Original name should fail.
|
||||
let pointer = JsonPointer::parse("/my_field").unwrap();
|
||||
assert!(e.resolve(pointer).is_err());
|
||||
|
||||
@@ -248,7 +248,8 @@ fn test_priority_regular_over_flattened() {
|
||||
|
||||
#[derive(JsonPointee)]
|
||||
struct Outer {
|
||||
my_field: i32, // Regular field with same name
|
||||
// Regular field with same name.
|
||||
my_field: i32,
|
||||
#[pointer(flatten)]
|
||||
inner: Inner,
|
||||
}
|
||||
@@ -485,12 +486,12 @@ fn test_pointer_to_chrono_datetime() {
|
||||
|
||||
let timestamp: DateTime<Utc> = "2024-01-15T10:30:00Z".parse().unwrap();
|
||||
|
||||
// Empty path should return the timestamp itself.
|
||||
// Empty pointer should return the timestamp itself.
|
||||
let pointer = JsonPointer::parse("").unwrap();
|
||||
let result = timestamp.resolve(pointer).unwrap();
|
||||
assert!(result.downcast_ref::<DateTime<Utc>>().is_some());
|
||||
assert!(result.is::<DateTime<Utc>>());
|
||||
|
||||
// Non-empty path should fail.
|
||||
// Non-empty pointer should fail.
|
||||
let pointer = JsonPointer::parse("/foo").unwrap();
|
||||
assert!(timestamp.resolve(pointer).is_err());
|
||||
}
|
||||
@@ -502,12 +503,12 @@ fn test_pointer_to_url() {
|
||||
|
||||
let url = Url::parse("https://example.com/path?query=value").unwrap();
|
||||
|
||||
// Empty path should return the URL itself.
|
||||
// Empty pointer should return the URL itself.
|
||||
let pointer = JsonPointer::parse("").unwrap();
|
||||
let result = url.resolve(pointer).unwrap();
|
||||
assert!(result.downcast_ref::<Url>().is_some());
|
||||
assert!(result.is::<Url>());
|
||||
|
||||
// Non-empty path should fail.
|
||||
// Non-empty pointer should fail.
|
||||
let pointer = JsonPointer::parse("/foo").unwrap();
|
||||
assert!(url.resolve(pointer).is_err());
|
||||
}
|
||||
@@ -529,24 +530,24 @@ fn test_pointer_to_serde_json() {
|
||||
// Test object field access.
|
||||
let pointer = JsonPointer::parse("/name").unwrap();
|
||||
let result = data.resolve(pointer).unwrap();
|
||||
assert!(result.downcast_ref::<serde_json::Value>().is_some());
|
||||
assert!(result.is::<serde_json::Value>());
|
||||
|
||||
// Test array index access.
|
||||
let pointer = JsonPointer::parse("/items/1").unwrap();
|
||||
let result = data.resolve(pointer).unwrap();
|
||||
assert!(result.downcast_ref::<serde_json::Value>().is_some());
|
||||
assert!(result.is::<serde_json::Value>());
|
||||
|
||||
// Test nested object access.
|
||||
let pointer = JsonPointer::parse("/nested/field").unwrap();
|
||||
let result = data.resolve(pointer).unwrap();
|
||||
assert!(result.downcast_ref::<serde_json::Value>().is_some());
|
||||
assert!(result.is::<serde_json::Value>());
|
||||
|
||||
// Test empty path returns the whole value.
|
||||
// Test empty pointer returns the whole value.
|
||||
let pointer = JsonPointer::parse("").unwrap();
|
||||
let result = data.resolve(pointer).unwrap();
|
||||
assert!(result.downcast_ref::<serde_json::Value>().is_some());
|
||||
assert!(result.is::<serde_json::Value>());
|
||||
|
||||
// Test non-existent key.
|
||||
// Test nonexistent key.
|
||||
let pointer = JsonPointer::parse("/nonexistent").unwrap();
|
||||
assert!(data.resolve(pointer).is_err());
|
||||
|
||||
@@ -574,12 +575,12 @@ fn test_indexmap() {
|
||||
let result = map.resolve(pointer).unwrap();
|
||||
assert_eq!(result.downcast_ref::<i32>(), Some(&20));
|
||||
|
||||
// Test empty path returns the map itself.
|
||||
// Test empty pointer returns the map itself.
|
||||
let pointer = JsonPointer::parse("").unwrap();
|
||||
let result = map.resolve(pointer).unwrap();
|
||||
assert!(result.downcast_ref::<IndexMap<String, i32>>().is_some());
|
||||
assert!(result.is::<IndexMap<String, i32>>());
|
||||
|
||||
// Test non-existent key.
|
||||
// Test nonexistent key.
|
||||
let pointer = JsonPointer::parse("/nonexistent").unwrap();
|
||||
assert!(map.resolve(pointer).is_err());
|
||||
}
|
||||
@@ -664,7 +665,7 @@ fn test_skip_with_rename_all() {
|
||||
let result = s.resolve(pointer).unwrap();
|
||||
assert_eq!(result.downcast_ref::<String>(), Some(&"hello".to_owned()));
|
||||
|
||||
// `hidden_field` should NOT be accessible (even as `hiddenField`).
|
||||
// `hidden_field` should not be accessible (even as `hiddenField`).
|
||||
let pointer = JsonPointer::parse("/hiddenField").unwrap();
|
||||
assert!(s.resolve(pointer).is_err());
|
||||
|
||||
@@ -782,7 +783,7 @@ fn test_skip_in_tuple_struct() {
|
||||
let result = t.resolve(pointer).unwrap();
|
||||
assert_eq!(result.downcast_ref::<String>(), Some(&"hello".to_owned()));
|
||||
|
||||
// Index 1 NOT accessible (skipped).
|
||||
// Index 1 not accessible (skipped).
|
||||
let pointer = JsonPointer::parse("/1").unwrap();
|
||||
assert!(t.resolve(pointer).is_err());
|
||||
|
||||
@@ -807,7 +808,7 @@ fn test_all_fields_skipped() {
|
||||
field2: 42,
|
||||
};
|
||||
|
||||
// Empty path should still resolve to self.
|
||||
// Empty pointer should still resolve to self.
|
||||
let pointer = JsonPointer::parse("").unwrap();
|
||||
assert!(s.resolve(pointer).is_ok());
|
||||
|
||||
@@ -834,7 +835,7 @@ fn test_skip_unit_variant() {
|
||||
let pointer = JsonPointer::parse("").unwrap();
|
||||
assert!(e.resolve(pointer).is_err());
|
||||
|
||||
// Non-skipped variant should work.
|
||||
// Non-skipped variant should succeed.
|
||||
let e = MyEnum::Active;
|
||||
let pointer = JsonPointer::parse("").unwrap();
|
||||
assert!(e.resolve(pointer).is_ok());
|
||||
@@ -926,7 +927,7 @@ fn test_multiple_variants_with_skip() {
|
||||
let s = Status::Pending;
|
||||
assert!(s.resolve(JsonPointer::parse("").unwrap()).is_err());
|
||||
|
||||
// Deleted blocked - both empty pointer and field access.
|
||||
// Deleted blocked, with both empty pointer and field access.
|
||||
let s = Status::Deleted {
|
||||
reason: "test".to_owned(),
|
||||
};
|
||||
@@ -943,8 +944,7 @@ fn test_multiple_variants_with_skip() {
|
||||
#[test]
|
||||
fn test_generic_type_with_bounds() {
|
||||
// Test that the derive macro correctly generates `JsonPointee` bounds for
|
||||
// generic type parameters. This mirrors the `RefOr<T>` type in the main
|
||||
// codebase.
|
||||
// generic type parameters. This mirrors the `RefOr<T>` type in Ploidy.
|
||||
#[derive(JsonPointee)]
|
||||
#[pointer(untagged)]
|
||||
enum GenericWrapper<T> {
|
||||
|
||||
@@ -321,12 +321,10 @@ fn test_newtype_variant_empty_pointer_returns_enum() {
|
||||
|
||||
let container = Container::Value("test".to_owned());
|
||||
|
||||
// Empty pointer should return the enum variant, not the inner `String`.
|
||||
// Empty pointer should return the enum variant, not the inner string.
|
||||
let pointer = JsonPointer::parse("").unwrap();
|
||||
let result = container.resolve(pointer).unwrap();
|
||||
|
||||
// This should succeed but not give us the inner `String` directly.
|
||||
assert!(result.downcast_ref::<String>().is_none());
|
||||
assert!(result.is::<Container>());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user