From ff8dcc507815404e5336b53ef59902163e67cf20 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 11 Jan 2026 02:37:47 +0000 Subject: [PATCH 1/8] Initial commit with task details Adding CLAUDE.md with task information for AI processing. This file will be removed when the task is complete. Issue: https://github.com/link-foundation/links-queue/issues/7 --- CLAUDE.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fb65e4a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +Issue to solve: https://github.com/link-foundation/links-queue/issues/7 +Your prepared branch: issue-7-80fcb6b536fe +Your prepared working directory: /tmp/gh-issue-solver-1768099065247 + +Proceed. From 2a0a6717ff7493707ed924295262c4b89f7be78a Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 11 Jan 2026 02:52:22 +0000 Subject: [PATCH 2/8] Add Link and LinkStore interfaces/traits (API Contract) This commit implements the core interfaces and traits for the Link data model and LinkStore operations as defined in issue #7. TypeScript (js/src/): - types.ts: Full interface definitions with JSDoc documentation - index.d.ts: Type declarations for Link, LinkRef, LinkStore, LinkPattern - index.js: Runtime implementations (createLink, matchesPattern, etc.) Rust (rust/src/): - traits.rs: LinkType, Link, LinkRef, LinkPattern, LinkStore traits - lib.rs: Public re-exports of all traits Key features: - Generic ID types (number/bigint/string in TS, generic T: LinkType in Rust) - Nested/recursive link structures (LinkRef can be ID or Link) - Universal links with optional values array (beyond source/target) - Pattern matching with Any wildcard support - Full API parity between JavaScript and Rust Compatible with: - links-notation (supports recursive structures via values) - doublets-rs (source/target model with generic IDs) Co-Authored-By: Claude Opus 4.5 --- js/examples/basic-usage.js | 167 +++++- js/src/index.d.ts | 157 +++++- js/src/index.js | 147 +++++- js/src/types.ts | 416 +++++++++++++++ js/tests/index.test.js | 247 ++++++++- rust/examples/basic_usage.rs | 185 ++++++- rust/src/lib.rs | 128 ++++- rust/src/traits.rs | 915 +++++++++++++++++++++++++++++++++ rust/tests/integration_test.rs | 235 ++++++++- 9 files changed, 2525 insertions(+), 72 deletions(-) create mode 100644 js/src/types.ts create mode 100644 rust/src/traits.rs diff --git a/js/examples/basic-usage.js b/js/examples/basic-usage.js index 755ce81..9dd234f 100644 --- a/js/examples/basic-usage.js +++ b/js/examples/basic-usage.js @@ -1,6 +1,7 @@ /** - * Basic usage example - * Demonstrates how to use the package + * Basic usage examples for links-queue + * + * This file demonstrates the core Link and LinkStore interfaces. * * Run with any runtime: * - Node.js: node examples/basic-usage.js @@ -8,20 +9,154 @@ * - Deno: deno run examples/basic-usage.js */ -import { add, multiply, delay } from '../src/index.js'; +import { + Any, + isLink, + isLinkId, + isLinkRef, + getLinkId, + createLink, + matchesPattern, +} from '../src/index.js'; + +// ============================================================================= +// Creating Links +// ============================================================================= + +console.log('=== Creating Links ===\n'); + +// Simple link with numeric IDs +const simpleLink = createLink(1, 2, 3); +console.log('Simple link:', simpleLink); +// Output: { id: 1, source: 2, target: 3 } + +// Link with bigint IDs (for large datasets) +const bigintLink = createLink(1n, 9007199254740993n, 9007199254740994n); +console.log('Bigint link:', bigintLink); + +// Link with string IDs (for UUID-based systems) +const uuidLink = createLink('link-001', 'source-uuid-123', 'target-uuid-456'); +console.log('UUID link:', uuidLink); + +// ============================================================================= +// Nested Links (Recursive Structures) +// ============================================================================= + +console.log('\n=== Nested Links ===\n'); + +// Create nested link structures for representing complex relationships +const personType = createLink(10, 0, 0); // A "type" link +const nameProperty = createLink(11, 0, 0); // A "name" property link +const person = createLink(1, personType, nameProperty); +console.log('Person link with type and name:', person); + +// Deep nesting: (1: (2: (3: 30 31))) +const level3 = createLink(3, 30, 31); +const level2 = createLink(2, level3, 21); +const level1 = createLink(1, level2, 11); +console.log('Deeply nested:', level1); +console.log(' Level 1 source ID:', level1.source.id); // 2 +console.log(' Level 2 source ID:', level1.source.source.id); // 3 + +// ============================================================================= +// Universal Links (Variable Number of References) +// ============================================================================= + +console.log('\n=== Universal Links ===\n'); + +// Universal link with additional values beyond source/target +// Useful for n-ary relationships +const tripleRelation = createLink( + 100, // id + 1, // source (subject) + 2, // target (predicate) + [3, 4, 5] // values (objects) +); +console.log('Triple+ relation:', tripleRelation); + +// ============================================================================= +// Type Checking +// ============================================================================= + +console.log('\n=== Type Checking ===\n'); + +console.log( + 'isLink({ id: 1, source: 2, target: 3 }):', + isLink({ id: 1, source: 2, target: 3 }) +); // true +console.log('isLink(42):', isLink(42)); // false +console.log('isLink(null):', isLink(null)); // false + +console.log('isLinkId(42):', isLinkId(42)); // true +console.log('isLinkId(1n):', isLinkId(1n)); // true +console.log('isLinkId("uuid"):', isLinkId('uuid')); // true +console.log('isLinkId({}):', isLinkId({})); // false + +console.log('isLinkRef(42):', isLinkRef(42)); // true +console.log('isLinkRef(simpleLink):', isLinkRef(simpleLink)); // true + +// ============================================================================= +// Extracting IDs +// ============================================================================= + +console.log('\n=== Extracting IDs ===\n'); + +console.log('getLinkId(42):', getLinkId(42)); // 42 +console.log('getLinkId(simpleLink):', getLinkId(simpleLink)); // 1 + +const nestedSource = createLink(5, 50, 51); +const linkWithNestedSource = createLink(1, nestedSource, 10); +console.log( + 'getLinkId(linkWithNestedSource.source):', + getLinkId(linkWithNestedSource.source) +); // 5 + +// ============================================================================= +// Pattern Matching +// ============================================================================= + +console.log('\n=== Pattern Matching ===\n'); + +const links = [ + createLink(1, 10, 20), + createLink(2, 10, 30), + createLink(3, 20, 30), + createLink(4, 30, 40), +]; + +// Find links with source = 10 +const sourcePattern = { source: 10 }; +const withSource10 = links.filter((link) => + matchesPattern(link, sourcePattern) +); +console.log( + 'Links with source=10:', + withSource10.map((l) => l.id) +); // [1, 2] + +// Find links with target = 30 +const targetPattern = { target: 30 }; +const withTarget30 = links.filter((link) => + matchesPattern(link, targetPattern) +); +console.log( + 'Links with target=30:', + withTarget30.map((l) => l.id) +); // [2, 3] -// Example: Using add function -console.log('Addition examples:'); -console.log(` 2 + 3 = ${add(2, 3)}`); -console.log(` -1 + 5 = ${add(-1, 5)}`); +// Find links with any source and target = 30 +const anySourcePattern = { source: Any, target: 30 }; +const anySourceTarget30 = links.filter((link) => + matchesPattern(link, anySourcePattern) +); +console.log( + 'Links with Any source, target=30:', + anySourceTarget30.map((l) => l.id) +); // [2, 3] -// Example: Using multiply function -console.log('\nMultiplication examples:'); -console.log(` 4 * 5 = ${multiply(4, 5)}`); -console.log(` -2 * 3 = ${multiply(-2, 3)}`); +// Match all links +const allPattern = { source: Any, target: Any }; +const allMatched = links.filter((link) => matchesPattern(link, allPattern)); +console.log('All links (Any, Any):', allMatched.length); // 4 -// Example: Using async delay function -console.log('\nAsync example:'); -console.log(' Waiting 100ms...'); -await delay(100); -console.log(' Done!'); +console.log('\n=== Done ===\n'); diff --git a/js/src/index.d.ts b/js/src/index.d.ts index 658cd82..c4b674e 100644 --- a/js/src/index.d.ts +++ b/js/src/index.d.ts @@ -1,27 +1,158 @@ /** - * Example module type definitions - * Replace this with your actual type definitions + * links-queue - Core type definitions + * + * This module exports the Link and LinkStore interfaces that form + * the API contract for all implementations. */ +// ============================================================================= +// ID and Reference Types +// ============================================================================= + +/** + * A link identifier. Can be a number, bigint, or string. + */ +export type LinkId = number | bigint | string; + +/** + * A reference to a link, which can be an ID or a nested Link object. + */ +export type LinkRef = LinkId | Link; + +// ============================================================================= +// Link Interface +// ============================================================================= + +/** + * Represents a link (associative data structure) connecting source to target. + */ +export interface Link { + readonly id: LinkId; + readonly source: LinkRef; + readonly target: LinkRef; + readonly values?: readonly LinkRef[]; +} + +// ============================================================================= +// Pattern Matching +// ============================================================================= + +/** + * Special value indicating "any" match in pattern queries. + */ +export declare const Any: unique symbol; +export type AnyType = typeof Any; + +/** + * Pattern for matching links in queries. + */ +export interface LinkPattern { + readonly id?: LinkId | AnyType; + readonly source?: LinkRef | AnyType; + readonly target?: LinkRef | AnyType; +} + +// ============================================================================= +// LinkStore Interface +// ============================================================================= + +/** + * Interface for link storage operations. + */ +export interface LinkStore { + create(source: LinkRef, target: LinkRef): Promise; + createWithValues( + source: LinkRef, + target: LinkRef, + values: readonly LinkRef[] + ): Promise; + get(id: LinkId): Promise; + exists(id: LinkId): Promise; + find(pattern: LinkPattern): Promise; + count(pattern?: LinkPattern): Promise; + update(id: LinkId, source: LinkRef, target: LinkRef): Promise; + delete(id: LinkId): Promise; + deleteMatching(pattern: LinkPattern): Promise; + iterate(pattern?: LinkPattern): AsyncIterable; +} + +// ============================================================================= +// Utility Types +// ============================================================================= + +export type LinkIdOf = L['id']; + +export interface MutableLink { + id: LinkId; + source: LinkRef; + target: LinkRef; + values?: LinkRef[]; +} + +export interface CreateLinkOptions { + source: LinkRef; + target: LinkRef; + values?: LinkRef[]; +} + +export type LinkResult = + | { success: true; value: T } + | { success: false; error: string }; + +// ============================================================================= +// Utility Functions +// ============================================================================= + +/** + * Checks if a value is a Link object. + */ +export declare const isLink: (value: unknown) => value is Link; + +/** + * Checks if a value is a valid LinkId. + */ +export declare const isLinkId: (value: unknown) => value is LinkId; + +/** + * Checks if a value is a valid LinkRef. + */ +export declare const isLinkRef: (value: unknown) => value is LinkRef; + +/** + * Extracts the LinkId from a LinkRef. + */ +export declare const getLinkId: (ref: LinkRef) => LinkId; + +/** + * Creates a Link object from source and target references. + */ +export declare const createLink: ( + id: LinkId, + source: LinkRef, + target: LinkRef, + values?: LinkRef[] +) => Link; + +/** + * Checks if a pattern matches a link. + */ +export declare const matchesPattern: (link: Link, pattern: LinkPattern) => boolean; + +// ============================================================================= +// Deprecated exports (keep for backward compatibility) +// ============================================================================= + /** - * Adds two numbers - * @param a - First number - * @param b - Second number - * @returns Sum of a and b + * @deprecated Use the new Link interface */ export declare const add: (a: number, b: number) => number; /** - * Multiplies two numbers - * @param a - First number - * @param b - Second number - * @returns Product of a and b + * @deprecated Use the new Link interface */ export declare const multiply: (a: number, b: number) => number; /** - * Delays execution for specified milliseconds - * @param ms - Milliseconds to wait - * @returns Promise that resolves after the delay + * @deprecated Will be removed in future versions */ export declare const delay: (ms: number) => Promise; diff --git a/js/src/index.js b/js/src/index.js index 22705fd..d1dd98e 100644 --- a/js/src/index.js +++ b/js/src/index.js @@ -1,10 +1,151 @@ /** - * Example module entry point - * Replace this with your actual implementation + * links-queue - Core module entry point + * + * This module exports the Link and LinkStore interface definitions + * along with utility functions and constants. */ +// ============================================================================= +// Pattern Matching Constants +// ============================================================================= + +/** + * Special value indicating "any" match in pattern queries. + * Use this in LinkPattern to match any value in that position. + * + * @example + * // Find all links with source = 5, any target + * const links = await store.find({ source: 5, target: Any }); + */ +export const Any = Symbol('Any'); + +// ============================================================================= +// Utility Functions +// ============================================================================= + +/** + * Checks if a value is a Link object (has id, source, target properties). + * + * @param {unknown} value - The value to check + * @returns {boolean} True if value is a Link object + * + * @example + * isLink({ id: 1, source: 2, target: 3 }) // true + * isLink(42) // false + * isLink("hello") // false + */ +export const isLink = (value) => + value !== null && + typeof value === 'object' && + 'id' in value && + 'source' in value && + 'target' in value; + +/** + * Checks if a value is a valid LinkId (number, bigint, or string). + * + * @param {unknown} value - The value to check + * @returns {boolean} True if value is a valid LinkId + * + * @example + * isLinkId(42) // true + * isLinkId(9007199254740993n) // true + * isLinkId("uuid-here") // true + * isLinkId({}) // false + */ +export const isLinkId = (value) => + typeof value === 'number' || + typeof value === 'bigint' || + typeof value === 'string'; + +/** + * Checks if a value is a valid LinkRef (LinkId or Link). + * + * @param {unknown} value - The value to check + * @returns {boolean} True if value is a valid LinkRef + * + * @example + * isLinkRef(42) // true + * isLinkRef({ id: 1, source: 2, target: 3 }) // true + */ +export const isLinkRef = (value) => isLinkId(value) || isLink(value); + +/** + * Extracts the LinkId from a LinkRef. + * If the ref is a Link, returns its id. Otherwise returns the ref itself. + * + * @param {import('./index.d.ts').LinkRef} ref - The reference to extract ID from + * @returns {import('./index.d.ts').LinkId} The extracted LinkId + * + * @example + * getLinkId(42) // 42 + * getLinkId({ id: 1, source: 2, target: 3 }) // 1 + */ +export const getLinkId = (ref) => (isLink(ref) ? ref.id : ref); + +/** + * Creates a Link object from source and target references. + * + * @param {import('./index.d.ts').LinkId} id - The link ID + * @param {import('./index.d.ts').LinkRef} source - The source reference + * @param {import('./index.d.ts').LinkRef} target - The target reference + * @param {import('./index.d.ts').LinkRef[]} [values] - Optional additional values + * @returns {import('./index.d.ts').Link} The created Link object + * + * @example + * const link = createLink(1, 2, 3); + * console.log(link); // { id: 1, source: 2, target: 3 } + * + * const universalLink = createLink(1, 2, 3, [4, 5]); + * console.log(universalLink); // { id: 1, source: 2, target: 3, values: [4, 5] } + */ +export const createLink = (id, source, target, values = undefined) => { + const link = { id, source, target }; + if (values !== undefined && values.length > 0) { + link.values = values; + } + return Object.freeze(link); +}; + +/** + * Checks if a pattern matches a link. + * + * @param {import('./index.d.ts').Link} link - The link to check + * @param {import('./index.d.ts').LinkPattern} pattern - The pattern to match against + * @returns {boolean} True if the link matches the pattern + * + * @example + * const link = { id: 1, source: 2, target: 3 }; + * matchesPattern(link, { source: 2 }) // true + * matchesPattern(link, { source: 5 }) // false + * matchesPattern(link, { source: Any }) // true + */ +export const matchesPattern = (link, pattern) => { + if (pattern.id !== undefined && pattern.id !== Any) { + if (getLinkId(link.id) !== getLinkId(pattern.id)) { + return false; + } + } + if (pattern.source !== undefined && pattern.source !== Any) { + if (getLinkId(link.source) !== getLinkId(pattern.source)) { + return false; + } + } + if (pattern.target !== undefined && pattern.target !== Any) { + if (getLinkId(link.target) !== getLinkId(pattern.target)) { + return false; + } + } + return true; +}; + +// ============================================================================= +// Backward Compatible Exports (deprecated) +// ============================================================================= + /** * Example function that adds two numbers + * @deprecated Use the new Link interfaces instead * @param {number} a - First number * @param {number} b - Second number * @returns {number} Sum of a and b @@ -13,6 +154,7 @@ export const add = (a, b) => a + b; /** * Example function that multiplies two numbers + * @deprecated Use the new Link interfaces instead * @param {number} a - First number * @param {number} b - Second number * @returns {number} Product of a and b @@ -21,6 +163,7 @@ export const multiply = (a, b) => a * b; /** * Example async function + * @deprecated Will be removed in future versions * @param {number} ms - Milliseconds to wait * @returns {Promise} */ diff --git a/js/src/types.ts b/js/src/types.ts new file mode 100644 index 0000000..118f4df --- /dev/null +++ b/js/src/types.ts @@ -0,0 +1,416 @@ +/** + * Core Link and LinkStore type definitions for links-queue. + * + * This module provides the foundational interfaces for the Link data model + * and LinkStore operations. These interfaces establish the API contract that + * implementations must follow. + * + * @module types + * + * Design goals: + * - Compatible with links-notation (supports nested/recursive structures) + * - Compatible with doublets-rs patterns (source/target model) + * - Extensible for universal links with any number of references + * + * @see https://github.com/link-foundation/links-notation + * @see https://github.com/linksplatform/doublets-rs + */ + +// ============================================================================= +// ID Types +// ============================================================================= + +/** + * A link identifier. Can be a number, bigint, or string. + * + * @example + * const numericId: LinkId = 42; + * const bigId: LinkId = 9007199254740993n; + * const stringId: LinkId = "custom-uuid-here"; + */ +export type LinkId = number | bigint | string; + +// ============================================================================= +// Link Reference Types +// ============================================================================= + +/** + * A reference to a link, which can be: + * - A link ID (direct reference by identifier) + * - A nested Link object (inline definition) + * + * This allows for flexible link compositions and nested structures, + * supporting arbitrary levels of recursion as required by links-notation. + * + * @example + * // Direct reference by ID + * const ref1: LinkRef = 42; + * + * // Nested link reference + * const ref2: LinkRef = { id: 1, source: 2, target: 3 }; + * + * // Deeply nested + * const ref3: LinkRef = { + * id: 1, + * source: { id: 2, source: 3, target: 4 }, + * target: 5 + * }; + */ +export type LinkRef = LinkId | Link; + +// ============================================================================= +// Link Interface +// ============================================================================= + +/** + * Represents a link (associative data structure) connecting source to target. + * + * A Link is the fundamental unit of data in links-queue. It represents + * a directed relationship from a source to a target, identified by a unique ID. + * + * For compatibility with links-notation's universal link model, additional + * values can be provided beyond the basic source/target pair. + * + * @property id - Unique identifier for this link + * @property source - Reference to the source (can be another Link or LinkId) + * @property target - Reference to the target (can be another Link or LinkId) + * @property values - Optional additional values for universal links (extensibility) + * + * @example + * // Simple doublet-style link + * const link: Link = { + * id: 1, + * source: 2, + * target: 3 + * }; + * + * @example + * // Nested link structure + * const nestedLink: Link = { + * id: 1, + * source: { id: 2, source: 0, target: 0 }, + * target: { id: 3, source: 4, target: 5 } + * }; + * + * @example + * // Universal link with additional values + * const universalLink: Link = { + * id: 1, + * source: 2, + * target: 3, + * values: [4, 5, { id: 6, source: 7, target: 8 }] + * }; + */ +export interface Link { + /** + * Unique identifier for this link. + */ + readonly id: LinkId; + + /** + * Reference to the source of this link. + * Can be a LinkId for direct reference or a nested Link for inline definition. + */ + readonly source: LinkRef; + + /** + * Reference to the target of this link. + * Can be a LinkId for direct reference or a nested Link for inline definition. + */ + readonly target: LinkRef; + + /** + * Optional additional values for universal links. + * Supports arbitrary number of references beyond source/target. + * Compatible with links-notation's values array model. + */ + readonly values?: readonly LinkRef[]; +} + +// ============================================================================= +// Pattern Matching Types +// ============================================================================= + +/** + * Special value indicating "any" match in pattern queries. + * When used in a LinkPattern, matches any value in that position. + */ +export const Any = Symbol('Any'); +export type AnyType = typeof Any; + +/** + * Pattern for matching links in queries. + * + * Uses `Any` symbol as a wildcard to match any value in that position. + * Each field can be: + * - A specific LinkRef value (exact match) + * - The `Any` symbol (matches anything) + * - undefined (not included in match criteria) + * + * @example + * // Match all links with source = 5 + * const pattern: LinkPattern = { source: 5 }; + * + * // Match all links with any source and target = 10 + * const pattern2: LinkPattern = { source: Any, target: 10 }; + * + * // Match all links (equivalent to no filter) + * const pattern3: LinkPattern = { source: Any, target: Any }; + */ +export interface LinkPattern { + /** + * Match criteria for link ID. + */ + readonly id?: LinkId | AnyType; + + /** + * Match criteria for link source. + */ + readonly source?: LinkRef | AnyType; + + /** + * Match criteria for link target. + */ + readonly target?: LinkRef | AnyType; +} + +// ============================================================================= +// LinkStore Interface +// ============================================================================= + +/** + * Interface for link storage operations. + * + * Provides CRUD operations for managing links, plus query capabilities + * with pattern matching support. This is the main interface that + * implementations must satisfy. + * + * The design follows the doublets-rs trait pattern while maintaining + * JavaScript/TypeScript idioms. + * + * @example + * class InMemoryLinkStore implements LinkStore { + * // ... implementation + * } + * + * const store: LinkStore = new InMemoryLinkStore(); + * const link = await store.create(1, 2); + * console.log(link.id); // auto-generated ID + */ +export interface LinkStore { + // --------------------------------------------------------------------------- + // Create Operations + // --------------------------------------------------------------------------- + + /** + * Creates a new link from source to target. + * + * The implementation should auto-generate a unique ID for the link. + * + * @param source - The source reference for the new link + * @param target - The target reference for the new link + * @returns The newly created Link with its assigned ID + * + * @example + * const link = await store.create(1, 2); + * console.log(link); // { id: 1, source: 1, target: 2 } + */ + create(source: LinkRef, target: LinkRef): Promise; + + /** + * Creates a new link with additional values (universal link). + * + * @param source - The source reference for the new link + * @param target - The target reference for the new link + * @param values - Additional value references + * @returns The newly created Link + */ + createWithValues( + source: LinkRef, + target: LinkRef, + values: readonly LinkRef[] + ): Promise; + + // --------------------------------------------------------------------------- + // Read Operations + // --------------------------------------------------------------------------- + + /** + * Retrieves a link by its ID. + * + * @param id - The ID of the link to retrieve + * @returns The Link if found, null otherwise + * + * @example + * const link = await store.get(42); + * if (link) { + * console.log(`Found: ${link.source} -> ${link.target}`); + * } + */ + get(id: LinkId): Promise; + + /** + * Checks if a link with the given ID exists. + * + * @param id - The ID to check + * @returns True if the link exists, false otherwise + * + * @example + * if (await store.exists(42)) { + * console.log('Link 42 exists'); + * } + */ + exists(id: LinkId): Promise; + + /** + * Finds links matching the given pattern. + * + * Supports wildcards via the `Any` symbol for flexible querying. + * + * @param pattern - The pattern to match against + * @returns Array of matching links + * + * @example + * // Find all links with source = 5 + * const links = await store.find({ source: 5 }); + * + * // Find all links with any source, target = 10 + * const links2 = await store.find({ source: Any, target: 10 }); + */ + find(pattern: LinkPattern): Promise; + + /** + * Counts links matching the given pattern. + * + * If no pattern is provided, counts all links. + * + * @param pattern - Optional pattern to match against + * @returns Count of matching links + * + * @example + * const total = await store.count(); + * const withSource5 = await store.count({ source: 5 }); + */ + count(pattern?: LinkPattern): Promise; + + // --------------------------------------------------------------------------- + // Update Operations + // --------------------------------------------------------------------------- + + /** + * Updates an existing link's source and/or target. + * + * @param id - The ID of the link to update + * @param source - New source reference + * @param target - New target reference + * @returns The updated Link + * @throws If the link with given ID does not exist + * + * @example + * const updated = await store.update(1, 10, 20); + * console.log(updated); // { id: 1, source: 10, target: 20 } + */ + update(id: LinkId, source: LinkRef, target: LinkRef): Promise; + + // --------------------------------------------------------------------------- + // Delete Operations + // --------------------------------------------------------------------------- + + /** + * Deletes a link by its ID. + * + * @param id - The ID of the link to delete + * @returns True if the link was deleted, false if it didn't exist + * + * @example + * const deleted = await store.delete(42); + * if (deleted) { + * console.log('Link 42 was deleted'); + * } + */ + delete(id: LinkId): Promise; + + /** + * Deletes all links matching the given pattern. + * + * @param pattern - The pattern to match for deletion + * @returns Count of deleted links + * + * @example + * // Delete all links with source = 5 + * const count = await store.deleteMatching({ source: 5 }); + * console.log(`Deleted ${count} links`); + */ + deleteMatching(pattern: LinkPattern): Promise; + + // --------------------------------------------------------------------------- + // Iteration Operations + // --------------------------------------------------------------------------- + + /** + * Iterates over all links matching the given pattern. + * + * Provides an async iterator for memory-efficient traversal of large datasets. + * + * @param pattern - Optional pattern to filter links + * @returns AsyncIterable of matching links + * + * @example + * for await (const link of store.iterate({ source: 5 })) { + * console.log(link); + * } + */ + iterate(pattern?: LinkPattern): AsyncIterable; +} + +// ============================================================================= +// Utility Types +// ============================================================================= + +/** + * Extracts the ID type from a Link type. + */ +export type LinkIdOf = L['id']; + +/** + * A mutable version of Link for internal use. + */ +export interface MutableLink { + id: LinkId; + source: LinkRef; + target: LinkRef; + values?: LinkRef[]; +} + +/** + * Options for creating a new link. + */ +export interface CreateLinkOptions { + /** + * The source reference for the new link. + */ + source: LinkRef; + + /** + * The target reference for the new link. + */ + target: LinkRef; + + /** + * Optional additional values for universal links. + */ + values?: LinkRef[]; +} + +/** + * Result type for operations that might fail. + */ +export type LinkResult = + | { success: true; value: T } + | { success: false; error: string }; + +// ============================================================================= +// Re-exports for convenience +// ============================================================================= + +export type { Link as ILink } from './types.js'; diff --git a/js/tests/index.test.js b/js/tests/index.test.js index 15fbcb6..030855f 100644 --- a/js/tests/index.test.js +++ b/js/tests/index.test.js @@ -1,12 +1,249 @@ /** - * Example test file using test-anywhere - * Works with Node.js, Bun, and Deno + * Test file for links-queue + * Works with Node.js, Bun, and Deno via test-anywhere */ import { describe, it, expect } from 'test-anywhere'; -import { add, multiply } from '../src/index.js'; +import { + add, + multiply, + Any, + isLink, + isLinkId, + isLinkRef, + getLinkId, + createLink, + matchesPattern, +} from '../src/index.js'; -describe('add function', () => { +// ============================================================================= +// Link and LinkRef Tests +// ============================================================================= + +describe('Link creation', () => { + it('should create a simple link with createLink', () => { + const link = createLink(1, 2, 3); + expect(link.id).toBe(1); + expect(link.source).toBe(2); + expect(link.target).toBe(3); + }); + + it('should create a link with bigint IDs', () => { + const link = createLink(1n, 2n, 3n); + expect(link.id).toBe(1n); + expect(link.source).toBe(2n); + expect(link.target).toBe(3n); + }); + + it('should create a link with string IDs', () => { + const link = createLink('link-1', 'source-id', 'target-id'); + expect(link.id).toBe('link-1'); + expect(link.source).toBe('source-id'); + expect(link.target).toBe('target-id'); + }); + + it('should create a link with nested link as source', () => { + const inner = createLink(2, 3, 4); + const outer = createLink(1, inner, 5); + expect(outer.id).toBe(1); + expect(outer.source).toEqual(inner); + expect(outer.target).toBe(5); + }); + + it('should create a universal link with values', () => { + const link = createLink(1, 2, 3, [4, 5, 6]); + expect(link.id).toBe(1); + expect(link.source).toBe(2); + expect(link.target).toBe(3); + expect(link.values).toEqual([4, 5, 6]); + }); + + it('should not include values property if values array is empty', () => { + const link = createLink(1, 2, 3, []); + expect(link.values).toBeUndefined(); + }); + + it('should freeze the created link', () => { + const link = createLink(1, 2, 3); + expect(Object.isFrozen(link)).toBe(true); + }); +}); + +describe('Deeply nested links', () => { + it('should support arbitrary nesting levels', () => { + const level3 = createLink(3, 30, 31); + const level2 = createLink(2, level3, 21); + const level1 = createLink(1, level2, 11); + + expect(level1.id).toBe(1); + expect(level1.source.id).toBe(2); + expect(level1.source.source.id).toBe(3); + expect(level1.source.source.source).toBe(30); + expect(level1.source.source.target).toBe(31); + }); +}); + +// ============================================================================= +// Type Check Functions Tests +// ============================================================================= + +describe('isLink', () => { + it('should return true for valid Link objects', () => { + expect(isLink({ id: 1, source: 2, target: 3 })).toBe(true); + expect(isLink(createLink(1, 2, 3))).toBe(true); + }); + + it('should return false for non-Link values', () => { + expect(isLink(null)).toBe(false); + expect(isLink(undefined)).toBe(false); + expect(isLink(42)).toBe(false); + expect(isLink('link')).toBe(false); + expect(isLink({})).toBe(false); + expect(isLink({ id: 1 })).toBe(false); + expect(isLink({ id: 1, source: 2 })).toBe(false); + }); +}); + +describe('isLinkId', () => { + it('should return true for valid LinkId types', () => { + expect(isLinkId(42)).toBe(true); + expect(isLinkId(0)).toBe(true); + expect(isLinkId(-1)).toBe(true); + expect(isLinkId(9007199254740993n)).toBe(true); + expect(isLinkId('uuid-here')).toBe(true); + expect(isLinkId('')).toBe(true); + }); + + it('should return false for non-LinkId values', () => { + expect(isLinkId(null)).toBe(false); + expect(isLinkId(undefined)).toBe(false); + expect(isLinkId({})).toBe(false); + expect(isLinkId([])).toBe(false); + expect(isLinkId(Symbol('test'))).toBe(false); + }); +}); + +describe('isLinkRef', () => { + it('should return true for LinkId values', () => { + expect(isLinkRef(42)).toBe(true); + expect(isLinkRef(1n)).toBe(true); + expect(isLinkRef('id')).toBe(true); + }); + + it('should return true for Link objects', () => { + expect(isLinkRef({ id: 1, source: 2, target: 3 })).toBe(true); + expect(isLinkRef(createLink(1, 2, 3))).toBe(true); + }); + + it('should return false for invalid values', () => { + expect(isLinkRef(null)).toBe(false); + expect(isLinkRef({})).toBe(false); + }); +}); + +describe('getLinkId', () => { + it('should return the ID directly for LinkId values', () => { + expect(getLinkId(42)).toBe(42); + expect(getLinkId(1n)).toBe(1n); + expect(getLinkId('uuid')).toBe('uuid'); + }); + + it('should extract ID from Link objects', () => { + const link = createLink(99, 2, 3); + expect(getLinkId(link)).toBe(99); + }); + + it('should extract ID from nested Link objects', () => { + const inner = createLink(10, 20, 30); + expect(getLinkId(inner)).toBe(10); + }); +}); + +// ============================================================================= +// Pattern Matching Tests +// ============================================================================= + +describe('matchesPattern', () => { + const link = createLink(1, 5, 10); + + it('should match when pattern is empty', () => { + expect(matchesPattern(link, {})).toBe(true); + }); + + it('should match by id', () => { + expect(matchesPattern(link, { id: 1 })).toBe(true); + expect(matchesPattern(link, { id: 2 })).toBe(false); + }); + + it('should match by source', () => { + expect(matchesPattern(link, { source: 5 })).toBe(true); + expect(matchesPattern(link, { source: 99 })).toBe(false); + }); + + it('should match by target', () => { + expect(matchesPattern(link, { target: 10 })).toBe(true); + expect(matchesPattern(link, { target: 99 })).toBe(false); + }); + + it('should match multiple fields', () => { + expect(matchesPattern(link, { source: 5, target: 10 })).toBe(true); + expect(matchesPattern(link, { source: 5, target: 99 })).toBe(false); + expect(matchesPattern(link, { id: 1, source: 5, target: 10 })).toBe(true); + }); + + it('should match Any wildcard for source', () => { + expect(matchesPattern(link, { source: Any })).toBe(true); + expect(matchesPattern(link, { source: Any, target: 10 })).toBe(true); + expect(matchesPattern(link, { source: Any, target: 99 })).toBe(false); + }); + + it('should match Any wildcard for target', () => { + expect(matchesPattern(link, { target: Any })).toBe(true); + expect(matchesPattern(link, { source: 5, target: Any })).toBe(true); + expect(matchesPattern(link, { source: 99, target: Any })).toBe(false); + }); + + it('should match Any wildcard for id', () => { + expect(matchesPattern(link, { id: Any })).toBe(true); + expect(matchesPattern(link, { id: Any, source: 5 })).toBe(true); + }); + + it('should match all with Any wildcards', () => { + expect(matchesPattern(link, { id: Any, source: Any, target: Any })).toBe( + true + ); + }); + + it('should match nested link reference by ID', () => { + const inner = createLink(5, 50, 51); + const outer = createLink(1, inner, 10); + + // Pattern matches source by extracting its ID + expect(matchesPattern(outer, { source: 5 })).toBe(true); + expect(matchesPattern(outer, { source: inner })).toBe(true); + }); +}); + +// ============================================================================= +// Any Symbol Tests +// ============================================================================= + +describe('Any symbol', () => { + it('should be a unique symbol', () => { + expect(typeof Any).toBe('symbol'); + expect(Any.toString()).toBe('Symbol(Any)'); + }); + + it('should be referentially equal to itself', () => { + expect(Any === Any).toBe(true); + }); +}); + +// ============================================================================= +// Backward Compatibility Tests (deprecated functions) +// ============================================================================= + +describe('add function (deprecated)', () => { it('should add two positive numbers', () => { expect(add(2, 3)).toBe(5); }); @@ -20,7 +257,7 @@ describe('add function', () => { }); }); -describe('multiply function', () => { +describe('multiply function (deprecated)', () => { it('should multiply two positive numbers', () => { expect(multiply(2, 3)).toBe(6); }); diff --git a/rust/examples/basic_usage.rs b/rust/examples/basic_usage.rs index c9729d3..6bc7038 100644 --- a/rust/examples/basic_usage.rs +++ b/rust/examples/basic_usage.rs @@ -1,34 +1,163 @@ //! Basic usage example for links-mq. //! -//! This example demonstrates the basic functionality of the package. +//! This example demonstrates the core Link and LinkStore interfaces. //! //! Run with: `cargo run --example basic_usage` -use links_mq::{add, delay, multiply}; - -#[tokio::main] -async fn main() { - // Example 1: Basic arithmetic - println!("Example 1: Basic arithmetic"); - println!("2 + 3 = {}", add(2, 3)); - println!("2 * 3 = {}", multiply(2, 3)); - println!(); - - // Example 2: Working with larger numbers - println!("Example 2: Working with larger numbers"); - println!("1000 + 2000 = {}", add(1000, 2000)); - println!("100 * 200 = {}", multiply(100, 200)); - println!(); - - // Example 3: Working with negative numbers - println!("Example 3: Working with negative numbers"); - println!("-5 + 10 = {}", add(-5, 10)); - println!("-3 * 4 = {}", multiply(-3, 4)); - println!(); - - // Example 4: Async delay - println!("Example 4: Async delay"); - println!("Waiting for 1 second..."); - delay(1.0).await; - println!("Done!"); +use links_mq::{Any, Link, LinkPattern, LinkRef}; + +fn main() { + // ========================================================================= + // Creating Links + // ========================================================================= + + println!("=== Creating Links ===\n"); + + // Simple link with u64 IDs + let simple_link = Link::new(1u64, LinkRef::Id(2), LinkRef::Id(3)); + println!("Simple link: {:?}", simple_link); + + // Link with u32 IDs (smaller memory footprint) + let u32_link = Link::new(1u32, LinkRef::Id(2u32), LinkRef::Id(3u32)); + println!("u32 link: {:?}", u32_link); + + // Link with usize IDs (platform-native size) + let usize_link = Link::new(1usize, LinkRef::Id(2usize), LinkRef::Id(3usize)); + println!("usize link: {:?}", usize_link); + + // ========================================================================= + // Nested Links (Recursive Structures) + // ========================================================================= + + println!("\n=== Nested Links ===\n"); + + // Create nested link structures for complex relationships + let inner = Link::new(10u64, LinkRef::Id(20), LinkRef::Id(30)); + let outer = Link::new(1u64, LinkRef::link(inner.clone()), LinkRef::Id(5)); + println!("Outer link with nested source: {:?}", outer); + println!(" Outer source ID: {}", outer.source_id()); // 10 + + // Deep nesting: (1: (2: (3: 30 31))) + let level3 = Link::new(3u64, LinkRef::Id(30), LinkRef::Id(31)); + let level2 = Link::new(2u64, LinkRef::link(level3), LinkRef::Id(21)); + let level1 = Link::new(1u64, LinkRef::link(level2), LinkRef::Id(11)); + println!("Deeply nested: {:?}", level1); + println!(" Level 1 source ID: {}", level1.source_id()); // 2 + + // ========================================================================= + // Universal Links (Variable Number of References) + // ========================================================================= + + println!("\n=== Universal Links ===\n"); + + // Universal link with additional values beyond source/target + let universal = Link::with_values( + 100u64, + LinkRef::Id(1), // subject + LinkRef::Id(2), // predicate + vec![LinkRef::Id(3), LinkRef::Id(4), LinkRef::Id(5)], // objects + ); + println!("Universal link: {:?}", universal); + println!(" Has values: {}", universal.has_values()); + + // ========================================================================= + // Special Link Types + // ========================================================================= + + println!("\n=== Special Link Types ===\n"); + + // Point link (self-referential: id == source == target) + let point = Link::::point(42); + println!("Point link: {:?}", point); + println!(" Is point: {}", point.is_point()); // true + + // Null/nothing link (all zeros) + let null_link = Link::::nothing(); + println!("Null link: {:?}", null_link); + println!(" Is null: {}", null_link.is_null()); // true + + // ========================================================================= + // LinkRef Operations + // ========================================================================= + + println!("\n=== LinkRef Operations ===\n"); + + // Create refs from IDs + let ref_id: LinkRef = LinkRef::Id(42); + println!("ID ref: {:?}", ref_id); + println!(" Is ID: {}", ref_id.is_id()); // true + println!(" Get ID: {}", ref_id.get_id()); // 42 + + // Create refs from links + let ref_link: LinkRef = LinkRef::link(simple_link.clone()); + println!("Link ref: {:?}", ref_link); + println!(" Is link: {}", ref_link.is_link()); // true + println!(" Get ID: {}", ref_link.get_id()); // 1 (the link's ID) + + // Using From trait + let from_id: LinkRef = 99u64.into(); + let from_link: LinkRef = simple_link.clone().into(); + println!("From ID: {:?}", from_id); + println!("From link: {:?}", from_link); + + // ========================================================================= + // Pattern Matching + // ========================================================================= + + println!("\n=== Pattern Matching ===\n"); + + let links = vec![ + Link::new(1u64, LinkRef::Id(10), LinkRef::Id(20)), + Link::new(2u64, LinkRef::Id(10), LinkRef::Id(30)), + Link::new(3u64, LinkRef::Id(20), LinkRef::Id(30)), + Link::new(4u64, LinkRef::Id(30), LinkRef::Id(40)), + ]; + + // Match all links + let all_pattern = LinkPattern::::all(); + let all_matched: Vec<_> = links.iter().filter(|l| all_pattern.matches(l)).collect(); + println!("All links count: {}", all_matched.len()); // 4 + + // Match by source + let source_pattern = LinkPattern::with_source(LinkRef::Id(10u64)); + let with_source_10: Vec<_> = links + .iter() + .filter(|l| source_pattern.matches(l)) + .map(|l| l.id) + .collect(); + println!("Links with source=10: {:?}", with_source_10); // [1, 2] + + // Match by target + let target_pattern = LinkPattern::with_target(LinkRef::Id(30u64)); + let with_target_30: Vec<_> = links + .iter() + .filter(|l| target_pattern.matches(l)) + .map(|l| l.id) + .collect(); + println!("Links with target=30: {:?}", with_target_30); // [2, 3] + + // Match with Any wildcard (builder pattern) + let any_source_pattern = LinkPattern::::new() + .source(Any) + .target(30u64); + let any_source_target_30: Vec<_> = links + .iter() + .filter(|l| any_source_pattern.matches(l)) + .map(|l| l.id) + .collect(); + println!("Links with Any source, target=30: {:?}", any_source_target_30); // [2, 3] + + // Match by source AND target + let exact_pattern = LinkPattern::with_source_target( + LinkRef::Id(10u64), + LinkRef::Id(20u64), + ); + let exact_match: Vec<_> = links + .iter() + .filter(|l| exact_pattern.matches(l)) + .map(|l| l.id) + .collect(); + println!("Links with source=10, target=20: {:?}", exact_match); // [1] + + println!("\n=== Done ===\n"); } diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 7478abb..44ff096 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,10 +1,73 @@ -//! Example module entry point. +//! links-queue - A lightweight message queue implementation. //! -//! Replace this with your actual implementation. +//! This crate provides the core Link and `LinkStore` abstractions for +//! building associative data structures and message queues. +//! +//! # Core Concepts +//! +//! - **Link**: A directed relationship from source to target, identified by a unique ID. +//! - **`LinkStore`**: A storage backend for managing links with CRUD operations. +//! - **`LinkPattern`**: A pattern for querying links with wildcard support. +//! +//! # Design Goals +//! +//! - Compatible with [links-notation](https://github.com/link-foundation/links-notation) +//! - Compatible with [doublets-rs](https://github.com/linksplatform/doublets-rs) patterns +//! - Extensible for universal links with any number of references +//! +//! # Example +//! +//! ```rust +//! use links_mq::{Link, LinkRef, LinkPattern, Any}; +//! +//! // Create a simple link +//! let link = Link::new(1u64, LinkRef::Id(2), LinkRef::Id(3)); +//! assert_eq!(link.id, 1); +//! assert_eq!(link.source_id(), 2); +//! assert_eq!(link.target_id(), 3); +//! +//! // Create a nested link structure +//! let inner = Link::new(2u64, LinkRef::Id(3), LinkRef::Id(4)); +//! let outer = Link::new(1u64, LinkRef::link(inner), LinkRef::Id(5)); +//! +//! // Pattern matching +//! let pattern = LinkPattern::with_source(LinkRef::Id(2u64)); +//! assert!(pattern.matches(&Link::new(1u64, LinkRef::Id(2), LinkRef::Id(3)))); +//! ``` + +// ============================================================================= +// Module Declarations +// ============================================================================= + +mod traits; + +// ============================================================================= +// Public Re-exports +// ============================================================================= + +pub use traits::{ + Any, + Link, + LinkError, + LinkPattern, + LinkRef, + LinkResult, + LinkStore, + LinkType, + PatternField, +}; + +// ============================================================================= +// Package Version +// ============================================================================= /// Package version (matches Cargo.toml version). pub const VERSION: &str = env!("CARGO_PKG_VERSION"); +// ============================================================================= +// Backward Compatible Exports (deprecated) +// ============================================================================= + /// Adds two numbers together. /// /// # Arguments @@ -22,6 +85,7 @@ pub const VERSION: &str = env!("CARGO_PKG_VERSION"); /// use links_mq::add; /// assert_eq!(add(2, 3), 5); /// ``` +#[deprecated(since = "0.2.0", note = "Use the Link and LinkStore traits instead")] #[must_use] pub const fn add(a: i64, b: i64) -> i64 { a + b @@ -44,6 +108,7 @@ pub const fn add(a: i64, b: i64) -> i64 { /// use links_mq::multiply; /// assert_eq!(multiply(2, 3), 6); /// ``` +#[deprecated(since = "0.2.0", note = "Use the Link and LinkStore traits instead")] #[must_use] pub const fn multiply(a: i64, b: i64) -> i64 { a * b @@ -65,16 +130,68 @@ pub const fn multiply(a: i64, b: i64) -> i64 { /// delay(0.1).await; /// } /// ``` +#[deprecated(since = "0.2.0", note = "Will be removed in future versions")] pub async fn delay(seconds: f64) { let duration = std::time::Duration::from_secs_f64(seconds); tokio::time::sleep(duration).await; } +// ============================================================================= +// Unit Tests +// ============================================================================= + #[cfg(test)] mod tests { use super::*; - mod add_tests { + mod link_api_tests { + use super::*; + + #[test] + fn test_link_creation() { + let link = Link::new(1u64, LinkRef::Id(2), LinkRef::Id(3)); + assert_eq!(link.id, 1); + assert_eq!(link.source_id(), 2); + assert_eq!(link.target_id(), 3); + } + + #[test] + fn test_link_with_values() { + let link = Link::with_values( + 1u64, + LinkRef::Id(2), + LinkRef::Id(3), + vec![LinkRef::Id(4), LinkRef::Id(5)], + ); + assert!(link.has_values()); + } + + #[test] + fn test_nested_link() { + let inner = Link::new(2u64, LinkRef::Id(3), LinkRef::Id(4)); + let outer = Link::new(1u64, LinkRef::link(inner), LinkRef::Id(5)); + assert_eq!(outer.source_id(), 2); + } + + #[test] + fn test_pattern_matching() { + let pattern = LinkPattern::with_source(LinkRef::Id(2u64)); + let link1 = Link::new(1u64, LinkRef::Id(2), LinkRef::Id(3)); + let link2 = Link::new(1u64, LinkRef::Id(5), LinkRef::Id(3)); + assert!(pattern.matches(&link1)); + assert!(!pattern.matches(&link2)); + } + + #[test] + fn test_any_pattern() { + let pattern = LinkPattern::::new().source(Any).target(3u64); + let link = Link::new(1u64, LinkRef::Id(999), LinkRef::Id(3)); + assert!(pattern.matches(&link)); + } + } + + #[allow(deprecated)] + mod backward_compat_tests { use super::*; #[test] @@ -96,10 +213,6 @@ mod tests { fn test_add_large_numbers() { assert_eq!(add(1_000_000, 2_000_000), 3_000_000); } - } - - mod multiply_tests { - use super::*; #[test] fn test_multiply_positive_numbers() { @@ -126,6 +239,7 @@ mod tests { use super::*; #[tokio::test] + #[allow(deprecated)] async fn test_delay() { let start = std::time::Instant::now(); delay(0.1).await; diff --git a/rust/src/traits.rs b/rust/src/traits.rs new file mode 100644 index 0000000..72ec8e8 --- /dev/null +++ b/rust/src/traits.rs @@ -0,0 +1,915 @@ +//! Core Link and `LinkStore` trait definitions for links-queue. +//! +//! This module provides the foundational traits for the Link data model +//! and `LinkStore` operations. These traits establish the API contract that +//! implementations must follow. +//! +//! # Design Goals +//! +//! - Compatible with [links-notation](https://github.com/link-foundation/links-notation) (supports nested/recursive structures) +//! - Compatible with [doublets-rs](https://github.com/linksplatform/doublets-rs) patterns (source/target model) +//! - Extensible for universal links with any number of references +//! - Generic over ID types for maximum flexibility +//! +//! # Example +//! +//! ```rust +//! use links_mq::{Link, LinkStore, LinkRef, LinkPattern, Any}; +//! +//! // Create a simple link +//! let link = Link::new(1u64, LinkRef::Id(2), LinkRef::Id(3)); +//! +//! // Pattern matching with wildcards +//! let pattern = LinkPattern::with_source(LinkRef::Id(2)); +//! ``` + +use std::fmt::Debug; +use std::hash::Hash; + +// ============================================================================= +// ID Type Trait +// ============================================================================= + +/// Trait for types that can be used as link identifiers. +/// +/// This trait defines the requirements for ID types, allowing the use of +/// various numeric types (u32, u64, usize, etc.) as link identifiers. +/// +/// # Implementors +/// +/// This trait is automatically implemented for common numeric types: +/// - `u8`, `u16`, `u32`, `u64`, `u128`, `usize` +/// - `i8`, `i16`, `i32`, `i64`, `i128`, `isize` +/// +/// Custom types can also implement this trait for use as link IDs. +pub trait LinkType: + Copy + Clone + Default + Debug + PartialEq + Eq + Hash + Send + Sync + 'static +{ + /// Returns the zero/default value for this type. + fn zero() -> Self; + + /// Checks if this value represents "nothing" (typically zero). + fn is_nothing(&self) -> bool; +} + +// Implement LinkType for common numeric types +macro_rules! impl_link_type { + ($($t:ty),*) => { + $( + impl LinkType for $t { + #[inline] + fn zero() -> Self { + 0 + } + + #[inline] + fn is_nothing(&self) -> bool { + *self == 0 + } + } + )* + }; +} + +impl_link_type!(u8, u16, u32, u64, u128, usize); +impl_link_type!(i8, i16, i32, i64, i128, isize); + +// ============================================================================= +// Link Reference Enum +// ============================================================================= + +/// A reference to a link, which can be an ID or a nested Link. +/// +/// This enum supports flexible link compositions and nested structures, +/// enabling arbitrary levels of recursion as required by links-notation. +/// +/// # Type Parameters +/// +/// * `T` - The link ID type (must implement [`LinkType`]) +/// +/// # Examples +/// +/// ```rust +/// use links_mq::{Link, LinkRef}; +/// +/// // Direct reference by ID +/// let ref1: LinkRef = LinkRef::Id(42); +/// +/// // Nested link reference +/// let inner = Link::new(1u64, LinkRef::Id(2), LinkRef::Id(3)); +/// let ref2: LinkRef = LinkRef::Link(Box::new(inner)); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum LinkRef { + /// Direct reference by link ID. + Id(T), + /// Nested link object (boxed to avoid infinite size). + Link(Box>), +} + +impl LinkRef { + /// Creates a new ID reference. + #[inline] + pub const fn id(id: T) -> Self { + Self::Id(id) + } + + /// Creates a new nested link reference. + #[inline] + pub fn link(link: Link) -> Self { + Self::Link(Box::new(link)) + } + + /// Returns the ID of this reference. + /// + /// If this is a nested link, returns the link's ID. + pub fn get_id(&self) -> T { + match self { + Self::Id(id) => *id, + Self::Link(link) => link.id, + } + } + + /// Returns true if this is an ID reference. + #[inline] + pub const fn is_id(&self) -> bool { + matches!(self, Self::Id(_)) + } + + /// Returns true if this is a nested link reference. + #[inline] + pub const fn is_link(&self) -> bool { + matches!(self, Self::Link(_)) + } + + /// Returns the ID if this is an ID reference, None otherwise. + pub const fn as_id(&self) -> Option { + match self { + Self::Id(id) => Some(*id), + Self::Link(_) => None, + } + } + + /// Returns a reference to the nested link if this is a link reference, None otherwise. + pub fn as_link(&self) -> Option<&Link> { + match self { + Self::Id(_) => None, + Self::Link(link) => Some(link), + } + } +} + +impl From for LinkRef { + fn from(id: T) -> Self { + Self::Id(id) + } +} + +impl From> for LinkRef { + fn from(link: Link) -> Self { + Self::Link(Box::new(link)) + } +} + +// ============================================================================= +// Link Struct +// ============================================================================= + +/// Represents a link (associative data structure) connecting source to target. +/// +/// A Link is the fundamental unit of data in links-queue. It represents +/// a directed relationship from a source to a target, identified by a unique ID. +/// +/// For compatibility with links-notation's universal link model, additional +/// values can be provided beyond the basic source/target pair. +/// +/// # Type Parameters +/// +/// * `T` - The link ID type (must implement [`LinkType`]) +/// +/// # Examples +/// +/// ```rust +/// use links_mq::{Link, LinkRef}; +/// +/// // Simple doublet-style link +/// let link = Link::new(1u64, LinkRef::Id(2), LinkRef::Id(3)); +/// +/// // Nested link structure +/// let inner = Link::new(2u64, LinkRef::Id(0), LinkRef::Id(0)); +/// let nested = Link::new(1u64, LinkRef::link(inner), LinkRef::Id(5)); +/// +/// // Universal link with additional values +/// let universal = Link::with_values( +/// 1u64, +/// LinkRef::Id(2), +/// LinkRef::Id(3), +/// vec![LinkRef::Id(4), LinkRef::Id(5)] +/// ); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Link { + /// Unique identifier for this link. + pub id: T, + /// Reference to the source of this link. + pub source: LinkRef, + /// Reference to the target of this link. + pub target: LinkRef, + /// Optional additional values for universal links. + pub values: Option>>, +} + +impl Link { + /// Creates a new link with the given ID, source, and target. + /// + /// # Arguments + /// + /// * `id` - The unique identifier for this link + /// * `source` - Reference to the source + /// * `target` - Reference to the target + /// + /// # Examples + /// + /// ```rust + /// use links_mq::{Link, LinkRef}; + /// + /// let link = Link::new(1u64, LinkRef::Id(2), LinkRef::Id(3)); + /// assert_eq!(link.id, 1); + /// ``` + #[inline] + pub const fn new(id: T, source: LinkRef, target: LinkRef) -> Self { + Self { + id, + source, + target, + values: None, + } + } + + /// Creates a new link with additional values (universal link). + /// + /// # Arguments + /// + /// * `id` - The unique identifier for this link + /// * `source` - Reference to the source + /// * `target` - Reference to the target + /// * `values` - Additional value references + /// + /// # Examples + /// + /// ```rust + /// use links_mq::{Link, LinkRef}; + /// + /// let link = Link::with_values( + /// 1u64, + /// LinkRef::Id(2), + /// LinkRef::Id(3), + /// vec![LinkRef::Id(4), LinkRef::Id(5)] + /// ); + /// assert!(link.values.is_some()); + /// ``` + #[inline] + pub fn with_values( + id: T, + source: LinkRef, + target: LinkRef, + values: Vec>, + ) -> Self { + Self { + id, + source, + target, + values: if values.is_empty() { None } else { Some(values) }, + } + } + + /// Creates a "point" link where id == source == target. + /// + /// A point link represents a self-referential entity. + /// + /// # Examples + /// + /// ```rust + /// use links_mq::Link; + /// + /// let point = Link::::point(42); + /// assert_eq!(point.source.get_id(), 42); + /// assert_eq!(point.target.get_id(), 42); + /// ``` + #[inline] + #[must_use] + pub const fn point(id: T) -> Self { + Self::new(id, LinkRef::Id(id), LinkRef::Id(id)) + } + + /// Creates a "null" link with all fields set to zero/default. + /// + /// # Examples + /// + /// ```rust + /// use links_mq::Link; + /// + /// let null_link = Link::::nothing(); + /// assert!(null_link.is_null()); + /// ``` + #[inline] + #[must_use] + pub fn nothing() -> Self { + let zero = T::zero(); + Self::new(zero, LinkRef::Id(zero), LinkRef::Id(zero)) + } + + /// Returns true if this link is a "point" (id == source == target). + pub fn is_point(&self) -> bool { + self.source.get_id() == self.id && self.target.get_id() == self.id + } + + /// Returns true if this is a null link (all fields are zero/nothing). + pub fn is_null(&self) -> bool { + self.id.is_nothing() + && self.source.get_id().is_nothing() + && self.target.get_id().is_nothing() + } + + /// Returns true if this link has additional values. + pub fn has_values(&self) -> bool { + self.values.as_ref().is_some_and(|v| !v.is_empty()) + } + + /// Returns the source ID. + pub fn source_id(&self) -> T { + self.source.get_id() + } + + /// Returns the target ID. + pub fn target_id(&self) -> T { + self.target.get_id() + } +} + +impl Default for Link { + fn default() -> Self { + Self::nothing() + } +} + +// ============================================================================= +// Pattern Matching +// ============================================================================= + +/// Represents "any" value in pattern matching. +/// +/// Use this in [`LinkPattern`] to match any value in that position. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] +pub struct Any; + +/// A pattern field that can match a specific value or any value. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Default)] +pub enum PatternField { + /// Match any value. + #[default] + Any, + /// Match a specific value. + Value(LinkRef), +} + + +impl PatternField { + /// Returns true if this field matches the given reference. + pub fn matches(&self, reference: &LinkRef) -> bool { + match self { + Self::Any => true, + Self::Value(pattern_ref) => pattern_ref.get_id() == reference.get_id(), + } + } +} + +impl From> for PatternField { + fn from(reference: LinkRef) -> Self { + Self::Value(reference) + } +} + +impl From for PatternField { + fn from(id: T) -> Self { + Self::Value(LinkRef::Id(id)) + } +} + +impl From for PatternField { + fn from(_: Any) -> Self { + Self::Any + } +} + +/// Pattern for matching links in queries. +/// +/// Each field can be set to match a specific value or use [`Any`] as a wildcard. +/// Fields set to [`PatternField::Any`] will match any value in that position. +/// +/// # Examples +/// +/// ```rust +/// use links_mq::{LinkPattern, LinkRef, Any}; +/// +/// // Match all links with source = 5 +/// let pattern = LinkPattern::::with_source(LinkRef::Id(5)); +/// +/// // Match all links with any source and target = 10 +/// let pattern2 = LinkPattern::::new() +/// .source(Any) +/// .target(10u64); +/// +/// // Match all links (equivalent to no filter) +/// let pattern3 = LinkPattern::::all(); +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] +pub struct LinkPattern { + /// Match criteria for link ID. + pub id: PatternField, + /// Match criteria for link source. + pub source: PatternField, + /// Match criteria for link target. + pub target: PatternField, +} + +impl LinkPattern { + /// Creates a new empty pattern (matches all links). + #[inline] + #[must_use] + pub fn new() -> Self { + Self::default() + } + + /// Creates a pattern that matches all links. + #[inline] + #[must_use] + pub fn all() -> Self { + Self::default() + } + + /// Creates a pattern that matches links with the given source. + pub const fn with_source(source: LinkRef) -> Self { + Self { + id: PatternField::Any, + source: PatternField::Value(source), + target: PatternField::Any, + } + } + + /// Creates a pattern that matches links with the given target. + pub const fn with_target(target: LinkRef) -> Self { + Self { + id: PatternField::Any, + source: PatternField::Any, + target: PatternField::Value(target), + } + } + + /// Creates a pattern that matches links with the given source and target. + pub const fn with_source_target(source: LinkRef, target: LinkRef) -> Self { + Self { + id: PatternField::Any, + source: PatternField::Value(source), + target: PatternField::Value(target), + } + } + + /// Sets the ID pattern field (builder pattern). + #[must_use] + pub fn id(mut self, id: impl Into>) -> Self { + self.id = id.into(); + self + } + + /// Sets the source pattern field (builder pattern). + #[must_use] + pub fn source(mut self, source: impl Into>) -> Self { + self.source = source.into(); + self + } + + /// Sets the target pattern field (builder pattern). + #[must_use] + pub fn target(mut self, target: impl Into>) -> Self { + self.target = target.into(); + self + } + + /// Checks if this pattern matches the given link. + pub fn matches(&self, link: &Link) -> bool { + self.id.matches(&LinkRef::Id(link.id)) + && self.source.matches(&link.source) + && self.target.matches(&link.target) + } +} + +// ============================================================================= +// Error Types +// ============================================================================= + +/// Errors that can occur during link store operations. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LinkError { + /// The requested link does not exist. + NotFound(T), + /// A link with this ID already exists. + AlreadyExists(T), + /// The link cannot be deleted because it has usages. + HasUsages(Vec>), + /// Storage limit has been reached. + LimitReached, + /// A custom error with a message. + Other(String), +} + +impl std::fmt::Display for LinkError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::NotFound(id) => write!(f, "Link with ID {id:?} not found"), + Self::AlreadyExists(id) => write!(f, "Link with ID {id:?} already exists"), + Self::HasUsages(links) => write!(f, "Link has {} usages", links.len()), + Self::LimitReached => write!(f, "Storage limit reached"), + Self::Other(msg) => write!(f, "{msg}"), + } + } +} + +impl std::error::Error for LinkError {} + +/// Result type for link store operations. +pub type LinkResult = Result>; + +// ============================================================================= +// LinkStore Trait +// ============================================================================= + +/// Trait for link storage operations. +/// +/// Provides CRUD operations for managing links, plus query capabilities +/// with pattern matching support. This is the main trait that +/// implementations must satisfy. +/// +/// The design follows the doublets-rs trait pattern while maintaining +/// Rust idioms and safety guarantees. +/// +/// # Type Parameters +/// +/// * `T` - The link ID type (must implement [`LinkType`]) +/// +/// # Example Implementation +/// +/// ```rust,ignore +/// use links_mq::{LinkStore, Link, LinkRef, LinkPattern, LinkResult, LinkError}; +/// +/// struct InMemoryLinkStore { +/// links: std::collections::HashMap>, +/// next_id: T, +/// } +/// +/// impl LinkStore for InMemoryLinkStore { +/// // ... implementation +/// } +/// ``` +pub trait LinkStore: Send + Sync { + // ------------------------------------------------------------------------- + // Create Operations + // ------------------------------------------------------------------------- + + /// Creates a new link from source to target. + /// + /// The implementation should auto-generate a unique ID for the link. + /// + /// # Arguments + /// + /// * `source` - The source reference for the new link + /// * `target` - The target reference for the new link + /// + /// # Returns + /// + /// The newly created Link with its assigned ID. + fn create(&mut self, source: LinkRef, target: LinkRef) -> LinkResult>; + + /// Creates a new link with additional values (universal link). + /// + /// # Arguments + /// + /// * `source` - The source reference for the new link + /// * `target` - The target reference for the new link + /// * `values` - Additional value references + /// + /// # Returns + /// + /// The newly created Link with its assigned ID. + fn create_with_values( + &mut self, + source: LinkRef, + target: LinkRef, + values: Vec>, + ) -> LinkResult>; + + // ------------------------------------------------------------------------- + // Read Operations + // ------------------------------------------------------------------------- + + /// Retrieves a link by its ID. + /// + /// # Arguments + /// + /// * `id` - The ID of the link to retrieve + /// + /// # Returns + /// + /// The Link if found, None otherwise. + fn get(&self, id: T) -> Option<&Link>; + + /// Checks if a link with the given ID exists. + /// + /// # Arguments + /// + /// * `id` - The ID to check + /// + /// # Returns + /// + /// True if the link exists, false otherwise. + fn exists(&self, id: T) -> bool { + self.get(id).is_some() + } + + /// Finds links matching the given pattern. + /// + /// Supports wildcards via the [`Any`] pattern for flexible querying. + /// + /// # Arguments + /// + /// * `pattern` - The pattern to match against + /// + /// # Returns + /// + /// Vector of references to matching links. + fn find(&self, pattern: &LinkPattern) -> Vec<&Link>; + + /// Counts links matching the given pattern. + /// + /// # Arguments + /// + /// * `pattern` - The pattern to match against + /// + /// # Returns + /// + /// Count of matching links. + fn count(&self, pattern: &LinkPattern) -> usize { + self.find(pattern).len() + } + + /// Returns the total number of links in the store. + fn total_count(&self) -> usize { + self.count(&LinkPattern::all()) + } + + // ------------------------------------------------------------------------- + // Update Operations + // ------------------------------------------------------------------------- + + /// Updates an existing link's source and/or target. + /// + /// # Arguments + /// + /// * `id` - The ID of the link to update + /// * `source` - New source reference + /// * `target` - New target reference + /// + /// # Returns + /// + /// The updated Link, or an error if the link doesn't exist. + fn update( + &mut self, + id: T, + source: LinkRef, + target: LinkRef, + ) -> LinkResult>; + + // ------------------------------------------------------------------------- + // Delete Operations + // ------------------------------------------------------------------------- + + /// Deletes a link by its ID. + /// + /// # Arguments + /// + /// * `id` - The ID of the link to delete + /// + /// # Returns + /// + /// True if the link was deleted, false if it didn't exist. + fn delete(&mut self, id: T) -> bool; + + /// Deletes all links matching the given pattern. + /// + /// # Arguments + /// + /// * `pattern` - The pattern to match for deletion + /// + /// # Returns + /// + /// Count of deleted links. + fn delete_matching(&mut self, pattern: &LinkPattern) -> usize; + + // ------------------------------------------------------------------------- + // Iteration Operations + // ------------------------------------------------------------------------- + + /// Returns an iterator over all links matching the given pattern. + /// + /// # Arguments + /// + /// * `pattern` - The pattern to filter links + /// + /// # Returns + /// + /// Iterator of references to matching links. + fn iter(&self, pattern: &LinkPattern) -> Box> + '_>; + + /// Returns an iterator over all links. + fn iter_all(&self) -> Box> + '_> { + self.iter(&LinkPattern::all()) + } +} + +// ============================================================================= +// Tests +// ============================================================================= + +#[cfg(test)] +mod tests { + use super::*; + + mod link_type_tests { + use super::*; + + #[test] + fn test_link_type_zero() { + assert_eq!(u64::zero(), 0u64); + assert_eq!(i32::zero(), 0i32); + } + + #[test] + fn test_link_type_is_nothing() { + assert!(0u64.is_nothing()); + assert!(!1u64.is_nothing()); + assert!(0i32.is_nothing()); + assert!(!(-1i32).is_nothing()); + } + } + + mod link_ref_tests { + use super::*; + + #[test] + fn test_link_ref_id() { + let ref_id: LinkRef = LinkRef::Id(42); + assert!(ref_id.is_id()); + assert!(!ref_id.is_link()); + assert_eq!(ref_id.get_id(), 42); + assert_eq!(ref_id.as_id(), Some(42)); + assert!(ref_id.as_link().is_none()); + } + + #[test] + fn test_link_ref_link() { + let inner = Link::new(1u64, LinkRef::Id(2), LinkRef::Id(3)); + let ref_link: LinkRef = LinkRef::link(inner.clone()); + assert!(!ref_link.is_id()); + assert!(ref_link.is_link()); + assert_eq!(ref_link.get_id(), 1); + assert!(ref_link.as_id().is_none()); + assert!(ref_link.as_link().is_some()); + } + + #[test] + fn test_link_ref_from_id() { + let ref_id: LinkRef = 42u64.into(); + assert_eq!(ref_id.get_id(), 42); + } + + #[test] + fn test_link_ref_from_link() { + let link = Link::new(1u64, LinkRef::Id(2), LinkRef::Id(3)); + let ref_link: LinkRef = link.into(); + assert_eq!(ref_link.get_id(), 1); + } + } + + mod link_tests { + use super::*; + + #[test] + fn test_link_new() { + let link = Link::new(1u64, LinkRef::Id(2), LinkRef::Id(3)); + assert_eq!(link.id, 1); + assert_eq!(link.source_id(), 2); + assert_eq!(link.target_id(), 3); + assert!(!link.has_values()); + } + + #[test] + fn test_link_with_values() { + let link = Link::with_values( + 1u64, + LinkRef::Id(2), + LinkRef::Id(3), + vec![LinkRef::Id(4), LinkRef::Id(5)], + ); + assert!(link.has_values()); + assert_eq!(link.values.as_ref().unwrap().len(), 2); + } + + #[test] + fn test_link_point() { + let point = Link::::point(42); + assert!(point.is_point()); + assert_eq!(point.id, 42); + assert_eq!(point.source_id(), 42); + assert_eq!(point.target_id(), 42); + } + + #[test] + fn test_link_nothing() { + let null_link = Link::::nothing(); + assert!(null_link.is_null()); + assert_eq!(null_link.id, 0); + } + + #[test] + fn test_link_nested() { + let inner = Link::new(2u64, LinkRef::Id(3), LinkRef::Id(4)); + let outer = Link::new(1u64, LinkRef::link(inner.clone()), LinkRef::Id(5)); + assert_eq!(outer.source_id(), 2); + assert_eq!(outer.target_id(), 5); + } + } + + mod pattern_tests { + use super::*; + + #[test] + fn test_pattern_matches_all() { + let pattern = LinkPattern::::all(); + let link = Link::new(1u64, LinkRef::Id(2), LinkRef::Id(3)); + assert!(pattern.matches(&link)); + } + + #[test] + fn test_pattern_with_source() { + let pattern = LinkPattern::with_source(LinkRef::Id(2u64)); + let link1 = Link::new(1u64, LinkRef::Id(2), LinkRef::Id(3)); + let link2 = Link::new(1u64, LinkRef::Id(5), LinkRef::Id(3)); + assert!(pattern.matches(&link1)); + assert!(!pattern.matches(&link2)); + } + + #[test] + fn test_pattern_with_target() { + let pattern = LinkPattern::with_target(LinkRef::Id(3u64)); + let link1 = Link::new(1u64, LinkRef::Id(2), LinkRef::Id(3)); + let link2 = Link::new(1u64, LinkRef::Id(2), LinkRef::Id(5)); + assert!(pattern.matches(&link1)); + assert!(!pattern.matches(&link2)); + } + + #[test] + fn test_pattern_builder() { + let pattern = LinkPattern::::new() + .source(2u64) + .target(Any); + let link = Link::new(1u64, LinkRef::Id(2), LinkRef::Id(3)); + assert!(pattern.matches(&link)); + } + + #[test] + fn test_pattern_with_source_target() { + let pattern = LinkPattern::with_source_target( + LinkRef::Id(2u64), + LinkRef::Id(3u64), + ); + let link1 = Link::new(1u64, LinkRef::Id(2), LinkRef::Id(3)); + let link2 = Link::new(1u64, LinkRef::Id(2), LinkRef::Id(5)); + assert!(pattern.matches(&link1)); + assert!(!pattern.matches(&link2)); + } + } + + mod error_tests { + use super::*; + + #[test] + fn test_error_display() { + let err: LinkError = LinkError::NotFound(42); + assert!(err.to_string().contains("42")); + + let err2: LinkError = LinkError::AlreadyExists(1); + assert!(err2.to_string().contains("already exists")); + } + } +} diff --git a/rust/tests/integration_test.rs b/rust/tests/integration_test.rs index 43fae7a..a288965 100644 --- a/rust/tests/integration_test.rs +++ b/rust/tests/integration_test.rs @@ -2,7 +2,240 @@ //! //! These tests verify the public API works correctly. +#[allow(deprecated)] use links_mq::{add, delay, multiply}; +use links_mq::{Any, Link, LinkPattern, LinkRef, LinkType, VERSION}; + +// ============================================================================= +// Link and LinkRef Integration Tests +// ============================================================================= + +mod link_integration_tests { + use super::*; + + #[test] + fn test_create_simple_link() { + let link = Link::new(1u64, LinkRef::Id(2), LinkRef::Id(3)); + assert_eq!(link.id, 1); + assert_eq!(link.source_id(), 2); + assert_eq!(link.target_id(), 3); + } + + #[test] + fn test_create_link_with_different_id_types() { + // Test with u32 + let link_u32 = Link::new(1u32, LinkRef::Id(2u32), LinkRef::Id(3u32)); + assert_eq!(link_u32.id, 1u32); + + // Test with u64 + let link_u64 = Link::new(1u64, LinkRef::Id(2u64), LinkRef::Id(3u64)); + assert_eq!(link_u64.id, 1u64); + + // Test with usize + let link_usize = Link::new(1usize, LinkRef::Id(2usize), LinkRef::Id(3usize)); + assert_eq!(link_usize.id, 1usize); + } + + #[test] + fn test_nested_link_structure() { + // Create inner link + let inner = Link::new(10u64, LinkRef::Id(20), LinkRef::Id(30)); + + // Create outer link with nested source + let outer = Link::new(1u64, LinkRef::link(inner), LinkRef::Id(5)); + + assert_eq!(outer.id, 1); + assert_eq!(outer.source_id(), 10); // Gets ID of nested link + assert_eq!(outer.target_id(), 5); + + // Verify the nested link is accessible + if let LinkRef::Link(nested) = &outer.source { + assert_eq!(nested.id, 10); + assert_eq!(nested.source_id(), 20); + assert_eq!(nested.target_id(), 30); + } else { + panic!("Expected nested link"); + } + } + + #[test] + fn test_deeply_nested_links() { + // Create a 3-level nested structure + let level3 = Link::new(3u64, LinkRef::Id(30), LinkRef::Id(31)); + let level2 = Link::new(2u64, LinkRef::link(level3), LinkRef::Id(21)); + let level1 = Link::new(1u64, LinkRef::link(level2), LinkRef::Id(11)); + + assert_eq!(level1.source_id(), 2); + + // Navigate to nested links + if let LinkRef::Link(l2) = &level1.source { + assert_eq!(l2.source_id(), 3); + if let LinkRef::Link(l3) = &l2.source { + assert_eq!(l3.source_id(), 30); + } else { + panic!("Expected level 3 nested link"); + } + } else { + panic!("Expected level 2 nested link"); + } + } + + #[test] + fn test_universal_link_with_values() { + let link = Link::with_values( + 1u64, + LinkRef::Id(2), + LinkRef::Id(3), + vec![LinkRef::Id(4), LinkRef::Id(5), LinkRef::Id(6)], + ); + + assert!(link.has_values()); + let values = link.values.as_ref().unwrap(); + assert_eq!(values.len(), 3); + assert_eq!(values[0].get_id(), 4); + assert_eq!(values[1].get_id(), 5); + assert_eq!(values[2].get_id(), 6); + } + + #[test] + fn test_point_link() { + let point = Link::::point(42); + assert!(point.is_point()); + assert_eq!(point.id, 42); + assert_eq!(point.source_id(), 42); + assert_eq!(point.target_id(), 42); + } + + #[test] + fn test_null_link() { + let null_link = Link::::nothing(); + assert!(null_link.is_null()); + assert_eq!(null_link.id, 0); + assert_eq!(null_link.source_id(), 0); + assert_eq!(null_link.target_id(), 0); + } + + #[test] + fn test_link_ref_conversions() { + // From ID + let ref_from_id: LinkRef = 42u64.into(); + assert!(ref_from_id.is_id()); + assert_eq!(ref_from_id.get_id(), 42); + + // From Link + let link = Link::new(1u64, LinkRef::Id(2), LinkRef::Id(3)); + let ref_from_link: LinkRef = link.into(); + assert!(ref_from_link.is_link()); + assert_eq!(ref_from_link.get_id(), 1); + } +} + +// ============================================================================= +// Pattern Matching Integration Tests +// ============================================================================= + +mod pattern_integration_tests { + use super::*; + + #[test] + fn test_match_all_pattern() { + let pattern = LinkPattern::::all(); + + // Should match any link + assert!(pattern.matches(&Link::new(1, LinkRef::Id(2), LinkRef::Id(3)))); + assert!(pattern.matches(&Link::new(100, LinkRef::Id(200), LinkRef::Id(300)))); + assert!(pattern.matches(&Link::point(42))); + assert!(pattern.matches(&Link::nothing())); + } + + #[test] + fn test_match_by_source() { + let pattern = LinkPattern::with_source(LinkRef::Id(5u64)); + + assert!(pattern.matches(&Link::new(1, LinkRef::Id(5), LinkRef::Id(10)))); + assert!(pattern.matches(&Link::new(2, LinkRef::Id(5), LinkRef::Id(20)))); + assert!(!pattern.matches(&Link::new(1, LinkRef::Id(10), LinkRef::Id(5)))); + } + + #[test] + fn test_match_by_target() { + let pattern = LinkPattern::with_target(LinkRef::Id(10u64)); + + assert!(pattern.matches(&Link::new(1, LinkRef::Id(5), LinkRef::Id(10)))); + assert!(pattern.matches(&Link::new(2, LinkRef::Id(20), LinkRef::Id(10)))); + assert!(!pattern.matches(&Link::new(1, LinkRef::Id(10), LinkRef::Id(5)))); + } + + #[test] + fn test_match_by_source_and_target() { + let pattern = LinkPattern::with_source_target(LinkRef::Id(5u64), LinkRef::Id(10u64)); + + assert!(pattern.matches(&Link::new(1, LinkRef::Id(5), LinkRef::Id(10)))); + assert!(!pattern.matches(&Link::new(1, LinkRef::Id(5), LinkRef::Id(20)))); + assert!(!pattern.matches(&Link::new(1, LinkRef::Id(20), LinkRef::Id(10)))); + } + + #[test] + fn test_builder_pattern() { + let pattern = LinkPattern::::new() + .id(1u64) + .source(Any) + .target(10u64); + + assert!(pattern.matches(&Link::new(1, LinkRef::Id(5), LinkRef::Id(10)))); + assert!(pattern.matches(&Link::new(1, LinkRef::Id(999), LinkRef::Id(10)))); + assert!(!pattern.matches(&Link::new(2, LinkRef::Id(5), LinkRef::Id(10)))); + assert!(!pattern.matches(&Link::new(1, LinkRef::Id(5), LinkRef::Id(20)))); + } + + #[test] + fn test_any_wildcard() { + let pattern = LinkPattern::::new().source(Any).target(Any); + + // Should match anything since both are Any + assert!(pattern.matches(&Link::new(1, LinkRef::Id(100), LinkRef::Id(200)))); + assert!(pattern.matches(&Link::new(1, LinkRef::Id(0), LinkRef::Id(0)))); + } +} + +// ============================================================================= +// LinkType Trait Tests +// ============================================================================= + +mod link_type_tests { + use super::*; + + #[test] + fn test_link_type_implementations() { + // All numeric types should implement LinkType + assert_eq!(u8::zero(), 0u8); + assert_eq!(u16::zero(), 0u16); + assert_eq!(u32::zero(), 0u32); + assert_eq!(u64::zero(), 0u64); + assert_eq!(usize::zero(), 0usize); + + assert_eq!(i8::zero(), 0i8); + assert_eq!(i16::zero(), 0i16); + assert_eq!(i32::zero(), 0i32); + assert_eq!(i64::zero(), 0i64); + assert_eq!(isize::zero(), 0isize); + } + + #[test] + fn test_is_nothing() { + assert!(0u64.is_nothing()); + assert!(!1u64.is_nothing()); + assert!(!42u64.is_nothing()); + + assert!(0i64.is_nothing()); + assert!(!1i64.is_nothing()); + assert!(!(-1i64).is_nothing()); + } +} + +// ============================================================================= +// Backward Compatibility Tests +// ============================================================================= mod add_integration_tests { use super::*; @@ -73,7 +306,7 @@ mod delay_integration_tests { } mod version_tests { - use links_mq::VERSION; + use super::*; #[test] fn test_version_is_not_empty() { From b92c90e939ed72eb0e89059d797f4f23787fce56 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 11 Jan 2026 02:54:42 +0000 Subject: [PATCH 3/8] Fix formatting in index.d.ts --- js/src/index.d.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/js/src/index.d.ts b/js/src/index.d.ts index c4b674e..610313c 100644 --- a/js/src/index.d.ts +++ b/js/src/index.d.ts @@ -136,7 +136,10 @@ export declare const createLink: ( /** * Checks if a pattern matches a link. */ -export declare const matchesPattern: (link: Link, pattern: LinkPattern) => boolean; +export declare const matchesPattern: ( + link: Link, + pattern: LinkPattern +) => boolean; // ============================================================================= // Deprecated exports (keep for backward compatibility) From 24b0184f8298db1fb0285470756b88d06f27f0a0 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 11 Jan 2026 02:57:03 +0000 Subject: [PATCH 4/8] Add changeset for Link and LinkStore interfaces --- js/.changeset/add-link-interfaces.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 js/.changeset/add-link-interfaces.md diff --git a/js/.changeset/add-link-interfaces.md b/js/.changeset/add-link-interfaces.md new file mode 100644 index 0000000..8c005b2 --- /dev/null +++ b/js/.changeset/add-link-interfaces.md @@ -0,0 +1,12 @@ +--- +'links-mq-js': minor +--- + +Add Link and LinkStore interfaces for Phase 1 API contract + +- Add `Link` interface with `id`, `source`, `target`, and optional `values` properties +- Add `LinkRef` and `LinkId` types for flexible link referencing +- Add `LinkStore` interface with CRUD operations (create, get, find, update, delete, etc.) +- Add `LinkPattern` interface with `Any` wildcard for pattern matching +- Add utility functions: `isLink`, `isLinkId`, `isLinkRef`, `getLinkId`, `createLink`, `matchesPattern` +- Compatible with links-notation and doublets-rs patterns From 9d9b8e289ad9b1d09b584fcb38f316687a10e287 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 11 Jan 2026 02:57:42 +0000 Subject: [PATCH 5/8] Revert "Initial commit with task details" This reverts commit ff8dcc507815404e5336b53ef59902163e67cf20. --- CLAUDE.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index fb65e4a..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,5 +0,0 @@ -Issue to solve: https://github.com/link-foundation/links-queue/issues/7 -Your prepared branch: issue-7-80fcb6b536fe -Your prepared working directory: /tmp/gh-issue-solver-1768099065247 - -Proceed. From ddb0615b0789849769026923b6ea9b1077457110 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 11 Jan 2026 05:17:11 +0100 Subject: [PATCH 6/8] Allow redundant_clone in test code for clarity Test code uses explicit clones for pedagogical clarity. Co-Authored-By: Claude Opus 4.5 --- rust/src/traits.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/rust/src/traits.rs b/rust/src/traits.rs index f66030c..fa33e51 100644 --- a/rust/src/traits.rs +++ b/rust/src/traits.rs @@ -742,6 +742,7 @@ pub trait LinkStore: Send + Sync { // ============================================================================= #[cfg(test)] +#[allow(clippy::redundant_clone)] mod tests { use super::*; From 9c99576dbfeba64a421e1ea920b3edcc2c4d812a Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 11 Jan 2026 05:20:34 +0100 Subject: [PATCH 7/8] Fix CI failures: update changeset package name and suppress deprecation warnings - Update changeset to use 'links-queue-js' instead of 'links-mq-js' - Add #![allow(deprecated)] to main.rs for backward compatibility demo - Add #[allow(deprecated)] to integration test modules for backward compat tests Co-Authored-By: Claude Opus 4.5 --- js/.changeset/add-link-interfaces.md | 2 +- rust/src/main.rs | 3 ++- rust/tests/integration_test.rs | 3 +++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/js/.changeset/add-link-interfaces.md b/js/.changeset/add-link-interfaces.md index 8c005b2..e65257a 100644 --- a/js/.changeset/add-link-interfaces.md +++ b/js/.changeset/add-link-interfaces.md @@ -1,5 +1,5 @@ --- -'links-mq-js': minor +'links-queue-js': minor --- Add Link and LinkStore interfaces for Phase 1 API contract diff --git a/rust/src/main.rs b/rust/src/main.rs index b563144..f8be813 100644 --- a/rust/src/main.rs +++ b/rust/src/main.rs @@ -2,7 +2,8 @@ //! //! This is a simple CLI that demonstrates the library functionality. -#[allow(deprecated)] +#![allow(deprecated)] + use links_queue::{add, delay, multiply}; #[tokio::main] diff --git a/rust/tests/integration_test.rs b/rust/tests/integration_test.rs index b35125e..48fd46f 100644 --- a/rust/tests/integration_test.rs +++ b/rust/tests/integration_test.rs @@ -237,6 +237,7 @@ mod link_type_tests { // Backward Compatibility Tests // ============================================================================= +#[allow(deprecated)] mod add_integration_tests { use super::*; @@ -256,6 +257,7 @@ mod add_integration_tests { } } +#[allow(deprecated)] mod multiply_integration_tests { use super::*; @@ -275,6 +277,7 @@ mod multiply_integration_tests { } } +#[allow(deprecated)] mod delay_integration_tests { use super::*; From 97b55743b05b4f1552e0266f91ff33263e70c274 Mon Sep 17 00:00:00 2001 From: konard Date: Sun, 11 Jan 2026 05:25:19 +0100 Subject: [PATCH 8/8] Apply cargo fmt formatting fixes Fix import ordering and code formatting to pass CI format check. Co-Authored-By: Claude Opus 4.5 --- rust/examples/basic_usage.rs | 18 ++++++++---------- rust/src/lib.rs | 10 +--------- rust/src/traits.rs | 32 ++++++++++++-------------------- rust/tests/integration_test.rs | 5 +---- 4 files changed, 22 insertions(+), 43 deletions(-) diff --git a/rust/examples/basic_usage.rs b/rust/examples/basic_usage.rs index 6ce6663..4088d1c 100644 --- a/rust/examples/basic_usage.rs +++ b/rust/examples/basic_usage.rs @@ -58,8 +58,8 @@ fn main() { // Universal link with additional values beyond source/target let universal = Link::with_values( 100u64, - LinkRef::Id(1), // subject - LinkRef::Id(2), // predicate + LinkRef::Id(1), // subject + LinkRef::Id(2), // predicate vec![LinkRef::Id(3), LinkRef::Id(4), LinkRef::Id(5)], // objects ); println!("Universal link: {:?}", universal); @@ -142,21 +142,19 @@ fn main() { println!("Links with target=30: {:?}", with_target_30); // [2, 3] // Match with Any wildcard (builder pattern) - let any_source_pattern = LinkPattern::::new() - .source(Any) - .target(30u64); + let any_source_pattern = LinkPattern::::new().source(Any).target(30u64); let any_source_target_30: Vec<_> = links .iter() .filter(|l| any_source_pattern.matches(l)) .map(|l| l.id) .collect(); - println!("Links with Any source, target=30: {:?}", any_source_target_30); // [2, 3] + println!( + "Links with Any source, target=30: {:?}", + any_source_target_30 + ); // [2, 3] // Match by source AND target - let exact_pattern = LinkPattern::with_source_target( - LinkRef::Id(10u64), - LinkRef::Id(20u64), - ); + let exact_pattern = LinkPattern::with_source_target(LinkRef::Id(10u64), LinkRef::Id(20u64)); let exact_match: Vec<_> = links .iter() .filter(|l| exact_pattern.matches(l)) diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 97b6c29..e850502 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -46,15 +46,7 @@ mod traits; // ============================================================================= pub use traits::{ - Any, - Link, - LinkError, - LinkPattern, - LinkRef, - LinkResult, - LinkStore, - LinkType, - PatternField, + Any, Link, LinkError, LinkPattern, LinkRef, LinkResult, LinkStore, LinkType, PatternField, }; // ============================================================================= diff --git a/rust/src/traits.rs b/rust/src/traits.rs index fa33e51..991e0ae 100644 --- a/rust/src/traits.rs +++ b/rust/src/traits.rs @@ -279,7 +279,11 @@ impl Link { id, source, target, - values: if values.is_empty() { None } else { Some(values) }, + values: if values.is_empty() { + None + } else { + Some(values) + }, } } @@ -313,7 +317,7 @@ impl Link { /// assert!(null_link.is_null()); /// ``` #[inline] - #[must_use] + #[must_use] pub fn nothing() -> Self { let zero = T::zero(); Self::new(zero, LinkRef::Id(zero), LinkRef::Id(zero)) @@ -364,8 +368,7 @@ impl Default for Link { pub struct Any; /// A pattern field that can match a specific value or any value. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -#[derive(Default)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] pub enum PatternField { /// Match any value. #[default] @@ -374,7 +377,6 @@ pub enum PatternField { Value(LinkRef), } - impl PatternField { /// Returns true if this field matches the given reference. pub fn matches(&self, reference: &LinkRef) -> bool { @@ -437,14 +439,14 @@ pub struct LinkPattern { impl LinkPattern { /// Creates a new empty pattern (matches all links). #[inline] - #[must_use] + #[must_use] pub fn new() -> Self { Self::default() } /// Creates a pattern that matches all links. #[inline] - #[must_use] + #[must_use] pub fn all() -> Self { Self::default() } @@ -683,12 +685,7 @@ pub trait LinkStore: Send + Sync { /// # Returns /// /// The updated Link, or an error if the link doesn't exist. - fn update( - &mut self, - id: T, - source: LinkRef, - target: LinkRef, - ) -> LinkResult>; + fn update(&mut self, id: T, source: LinkRef, target: LinkRef) -> LinkResult>; // ------------------------------------------------------------------------- // Delete Operations @@ -881,19 +878,14 @@ mod tests { #[test] fn test_pattern_builder() { - let pattern = LinkPattern::::new() - .source(2u64) - .target(Any); + let pattern = LinkPattern::::new().source(2u64).target(Any); let link = Link::new(1u64, LinkRef::Id(2), LinkRef::Id(3)); assert!(pattern.matches(&link)); } #[test] fn test_pattern_with_source_target() { - let pattern = LinkPattern::with_source_target( - LinkRef::Id(2u64), - LinkRef::Id(3u64), - ); + let pattern = LinkPattern::with_source_target(LinkRef::Id(2u64), LinkRef::Id(3u64)); let link1 = Link::new(1u64, LinkRef::Id(2), LinkRef::Id(3)); let link2 = Link::new(1u64, LinkRef::Id(2), LinkRef::Id(5)); assert!(pattern.matches(&link1)); diff --git a/rust/tests/integration_test.rs b/rust/tests/integration_test.rs index 48fd46f..af2f50b 100644 --- a/rust/tests/integration_test.rs +++ b/rust/tests/integration_test.rs @@ -177,10 +177,7 @@ mod pattern_integration_tests { #[test] fn test_builder_pattern() { - let pattern = LinkPattern::::new() - .id(1u64) - .source(Any) - .target(10u64); + let pattern = LinkPattern::::new().id(1u64).source(Any).target(10u64); assert!(pattern.matches(&Link::new(1, LinkRef::Id(5), LinkRef::Id(10)))); assert!(pattern.matches(&Link::new(1, LinkRef::Id(999), LinkRef::Id(10))));