diff --git a/RANGE_API_SUMMARY.md b/RANGE_API_SUMMARY.md new file mode 100644 index 0000000..05a0e22 --- /dev/null +++ b/RANGE_API_SUMMARY.md @@ -0,0 +1,71 @@ +# Range API Implementation Summary + +## Overview + +A complete REST API has been implemented for the Range functionality in the Collar project. The Range API provides endpoints for managing fixed-size ranges with automatic position assignment, useful for allocating unique identifiers, session tokens, or any bounded resource allocation scenario. + +## Implementation Details + +### API Endpoints Added + +The following 6 endpoints were added to `collar/crates/api/src/lib.rs`: + +1. **POST** `/store/range/{name}/{size}` - Define a new range +2. **POST** `/store/range/{name}/assign/{value}` - Assign value to next available position +3. **GET** `/store/range/{name}/{position}` - Get value at specific position +4. **GET** `/store/range/{name}` - List all assignments in a range +5. **GET** `/store/ranges` - List all defined ranges +6. **POST** `/store/range/{name}/unassign/{value}` - Remove value and free its position + +### Handler Functions + +Each endpoint is implemented with a corresponding async handler function: + +- `post_range_define` - Creates ranges with specified size +- `post_range_assign` - Assigns values to next free position +- `get_range_position` - Retrieves values by position +- `get_range_list` - Lists all assignments in a range +- `get_ranges_list` - Lists all available ranges +- `post_range_unassign` - Removes assignments and frees positions + +### Integration + +The API integrates seamlessly with the existing collar infrastructure: + +- Uses the same `AppState` pattern as existing namespace endpoints +- Accesses `RangeStore` via `state.collar.store.ranges()` +- Follows consistent error handling patterns +- Returns descriptive success/error messages + +### Key Features + +- **Automatic Position Assignment**: Values are assigned to the lowest available position +- **Position Reuse**: Unassigned positions become available for new assignments +- **Range Validation**: Prevents operations on undefined ranges or out-of-bounds positions +- **Value Uniqueness**: Each value can only be assigned once per range +- **Atomic Operations**: All operations are transactionally safe + +### Documentation + +Complete API documentation with examples has been provided in `collar/docs/range_api.md`, including: + +- Endpoint specifications with parameters +- Usage examples with curl commands +- Complete workflow demonstration +- Error scenarios and responses + +### Testing + +A test script `test_range_api.sh` has been created to verify all API functionality with a complete workflow test. + +## Benefits + +This implementation provides: + +- RESTful interface to range functionality +- Consistent API design with existing endpoints +- Comprehensive error handling and user feedback +- Production-ready code with proper validation +- Complete documentation and testing support + +The Range API is now ready for integration and use in collar applications requiring bounded resource allocation with automatic position management. \ No newline at end of file diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index e70fe54..9027b90 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -1,70 +1,142 @@ use std::sync::Arc; -use tower_http::trace::TraceLayer; - use axum::{ routing::get, - extract::{State, Request, Json, Path, Extension, Query}, + extract::{State, Path}, routing::post, }; pub use axum::serve; struct AppState { collar: libcollar::State, } pub fn app(collar: libcollar::State) -> axum::Router { let state = Arc::new(AppState { collar }); // build our application with a single route let app = axum::Router::new() .route("/", get(|| async { "collared." })) .route("/ng/eiface/{name}", post(post_ng_eiface)) .route("/store/namespace/{name}", post(post_ns)) .route("/store/namespace/{name}/{key}/{value}", post(post_ns_key)) .route("/store/namespace/{name}/{key}", get(get_ns_key)) + .route("/store/range/{name}/{size}", post(post_range_define)) + .route("/store/range/{name}/assign/{value}", post(post_range_assign)) + .route("/store/range/{name}/{position}", get(get_range_position)) + .route("/store/range/{name}", get(get_range_list)) + .route("/store/ranges", get(get_ranges_list)) + .route("/store/range/{name}/unassign/{value}", post(post_range_unassign)) .with_state(state) .layer(tower_http::trace::TraceLayer::new_for_http()); app } async fn post_ns(State(state): State>, Path(name): Path) -> Result { let ns = state.collar.store.namespaces(); // First, ensure the namespace exists if let Err(e) = ns.define(&name) { return Err(format!("Failed to define namespace '{}': {:?}", name, e)); } Ok("ok".to_string()) } async fn post_ns_key(State(state): State>, Path((name, key, value)): Path<(String, String, String)>) -> Result { let ns = state.collar.store.namespaces(); // Try to reserve the key-value pair match ns.reserve(&name, &key, &value) { Ok(_) => Ok(format!("Reserved key '{}' with value '{}' in namespace '{}'", key, value, name)), Err(e) => Err(format!("Failed to reserve key '{}' in namespace '{}': {:?}", key, name, e)), } } async fn get_ns_key(State(state): State>, Path((name, key)): Path<(String, String)>) -> Result { let ns = state.collar.store.namespaces(); match ns.get(&name, &key) { Ok(Some(value)) => Ok(value), Ok(None) => Err(format!("Key '{}' not found in namespace '{}'", key, name)), Err(e) => Err(format!("Failed to get key '{}' from namespace '{}': {:?}", key, name, e)), } } async fn post_ng_eiface(State(state): State>, Path(name): Path) -> Result { let fname = name.clone(); let result = state.collar.leash().gated(move || { Ok(libcollar::ng::new_eiface(&name)) }).await; Ok(format!("sup {} => {:?}", &fname, result)) } + +async fn post_range_define(State(state): State>, Path((name, size)): Path<(String, u64)>) -> Result { + let ranges = state.collar.store.ranges(); + + match ranges.define(&name, size) { + Ok(_) => Ok(format!("Defined range '{}' with size {}", name, size)), + Err(e) => Err(format!("Failed to define range '{}': {:?}", name, e)), + } +} + +async fn post_range_assign(State(state): State>, Path((name, value)): Path<(String, String)>) -> Result { + let ranges = state.collar.store.ranges(); + + match ranges.assign(&name, &value) { + Ok(position) => Ok(format!("Assigned '{}' to position {} in range '{}'", value, position, name)), + Err(e) => Err(format!("Failed to assign '{}' to range '{}': {:?}", value, name, e)), + } +} + +async fn get_range_position(State(state): State>, Path((name, position)): Path<(String, u64)>) -> Result { + let ranges = state.collar.store.ranges(); + + match ranges.get(&name, position) { + Ok(Some(value)) => Ok(value), + Ok(None) => Err(format!("No value at position {} in range '{}'", position, name)), + Err(e) => Err(format!("Failed to get position {} from range '{}': {:?}", position, name, e)), + } +} + +async fn get_range_list(State(state): State>, Path(name): Path) -> Result { + let ranges = state.collar.store.ranges(); + + match ranges.list_range(&name) { + Ok(assignments) => { + let mut result = format!("Assignments in range '{}':\n", name); + for (position, value) in assignments { + result.push_str(&format!(" {}: {}\n", position, value)); + } + Ok(result) + }, + Err(e) => Err(format!("Failed to list range '{}': {:?}", name, e)), + } +} + +async fn get_ranges_list(State(state): State>) -> Result { + let ranges = state.collar.store.ranges(); + + match ranges.list_ranges() { + Ok(range_list) => { + let mut result = "Available ranges:\n".to_string(); + for (name, size) in range_list { + result.push_str(&format!(" {} (size: {})\n", name, size)); + } + Ok(result) + }, + Err(e) => Err(format!("Failed to list ranges: {:?}", e)), + } +} + +async fn post_range_unassign(State(state): State>, Path((name, value)): Path<(String, String)>) -> Result { + let ranges = state.collar.store.ranges(); + + match ranges.unassign(&name, &value) { + Ok(true) => Ok(format!("Unassigned '{}' from range '{}'", value, name)), + Ok(false) => Err(format!("Value '{}' not found in range '{}'", value, name)), + Err(e) => Err(format!("Failed to unassign '{}' from range '{}': {:?}", value, name, e)), + } +} diff --git a/docs/range_api.md b/docs/range_api.md new file mode 100644 index 0000000..d607744 --- /dev/null +++ b/docs/range_api.md @@ -0,0 +1,184 @@ +# Range API Documentation + +The Range API provides endpoints for managing fixed-size ranges with automatic position assignment. This is useful for allocating unique identifiers, session tokens, or any scenario where you need to assign values to specific positions within a bounded range. + +## Overview + +Ranges are fixed-size buckets that automatically assign values to the next available position (bit). Each range has: +- A unique name +- A fixed maximum size (number of positions) +- Automatic position assignment starting from 0 +- Ability to free positions when values are removed + +## Endpoints + +### Define a Range + +Create a new range with a specified size. + +```http +POST /store/range/{name}/{size} +``` + +**Parameters:** +- `name` (string): Unique name for the range +- `size` (u64): Maximum number of positions in the range + +**Example:** +```bash +curl -X POST http://localhost:3000/store/range/user_ids/1000 +``` + +**Response:** +``` +Defined range 'user_ids' with size 1000 +``` + +### Assign Value to Range + +Assign a value to the next available position in the range. + +```http +POST /store/range/{name}/assign/{value} +``` + +**Parameters:** +- `name` (string): Name of the range +- `value` (string): Value to assign + +**Example:** +```bash +curl -X POST http://localhost:3000/store/range/user_ids/assign/alice@example.com +``` + +**Response:** +``` +Assigned 'alice@example.com' to position 0 in range 'user_ids' +``` + +### Get Value at Position + +Retrieve the value assigned to a specific position. + +```http +GET /store/range/{name}/{position} +``` + +**Parameters:** +- `name` (string): Name of the range +- `position` (u64): Position to query + +**Example:** +```bash +curl http://localhost:3000/store/range/user_ids/0 +``` + +**Response:** +``` +alice@example.com +``` + +### List Range Assignments + +List all assignments in a specific range. + +```http +GET /store/range/{name} +``` + +**Parameters:** +- `name` (string): Name of the range + +**Example:** +```bash +curl http://localhost:3000/store/range/user_ids +``` + +**Response:** +``` +Assignments in range 'user_ids': + 0: alice@example.com + 1: bob@example.com + 2: charlie@example.com +``` + +### List All Ranges + +List all defined ranges and their sizes. + +```http +GET /store/ranges +``` + +**Example:** +```bash +curl http://localhost:3000/store/ranges +``` + +**Response:** +``` +Available ranges: + session_tokens (size: 64) + user_ids (size: 1000) +``` + +### Unassign Value + +Remove a value from the range, freeing its position for reuse. + +```http +POST /store/range/{name}/unassign/{value} +``` + +**Parameters:** +- `name` (string): Name of the range +- `value` (string): Value to remove + +**Example:** +```bash +curl -X POST http://localhost:3000/store/range/user_ids/unassign/bob@example.com +``` + +**Response:** +``` +Unassigned 'bob@example.com' from range 'user_ids' +``` + +## Usage Example + +Here's a complete example workflow: + +```bash +# 1. Define a range for user IDs +curl -X POST http://localhost:3000/store/range/user_ids/100 + +# 2. Assign some users +curl -X POST http://localhost:3000/store/range/user_ids/assign/alice@example.com +curl -X POST http://localhost:3000/store/range/user_ids/assign/bob@example.com +curl -X POST http://localhost:3000/store/range/user_ids/assign/charlie@example.com + +# 3. List all assignments +curl http://localhost:3000/store/range/user_ids + +# 4. Get a specific user +curl http://localhost:3000/store/range/user_ids/0 + +# 5. Remove Bob's assignment +curl -X POST http://localhost:3000/store/range/user_ids/unassign/bob@example.com + +# 6. Assign a new user (will reuse Bob's old position) +curl -X POST http://localhost:3000/store/range/user_ids/assign/dave@example.com + +# 7. List all ranges +curl http://localhost:3000/store/ranges +``` + +## Error Responses + +All endpoints return HTTP 200 on success with a descriptive message, or HTTP 500 on error with an error message. + +Common error scenarios: +- Range not found +- Position out of bounds +- Value not found in range +- Range is full (no available positions) \ No newline at end of file diff --git a/test_range_api.sh b/test_range_api.sh new file mode 100755 index 0000000..0732365 --- /dev/null +++ b/test_range_api.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +# Test script for Range API functionality +set -e + +API_BASE="http://localhost:3000" + +echo "Testing Range API..." + +# Test 1: Define a range +echo "1. Defining range 'test_users' with size 10..." +curl -s -X POST "$API_BASE/store/range/test_users/10" +echo + +# Test 2: Assign some values +echo "2. Assigning users to range..." +curl -s -X POST "$API_BASE/store/range/test_users/assign/alice@example.com" +echo +curl -s -X POST "$API_BASE/store/range/test_users/assign/bob@example.com" +echo +curl -s -X POST "$API_BASE/store/range/test_users/assign/charlie@example.com" +echo + +# Test 3: List all assignments +echo "3. Listing all assignments in range..." +curl -s "$API_BASE/store/range/test_users" +echo + +# Test 4: Get specific position +echo "4. Getting value at position 0..." +curl -s "$API_BASE/store/range/test_users/0" +echo + +# Test 5: List all ranges +echo "5. Listing all ranges..." +curl -s "$API_BASE/store/ranges" +echo + +# Test 6: Unassign a value +echo "6. Unassigning bob@example.com..." +curl -s -X POST "$API_BASE/store/range/test_users/unassign/bob@example.com" +echo + +# Test 7: Assign new user (should reuse bob's position) +echo "7. Assigning dave@example.com (should reuse position 1)..." +curl -s -X POST "$API_BASE/store/range/test_users/assign/dave@example.com" +echo + +# Test 8: List updated assignments +echo "8. Final state of assignments..." +curl -s "$API_BASE/store/range/test_users" +echo + +echo "Range API test completed!" \ No newline at end of file