Skip to main content

sqry_core/graph/unified/edge/
kind.rs

1//! `EdgeKind` enumeration for the unified graph architecture.
2//!
3//! This module defines `EdgeKind`, which categorizes all relationship types
4//! that can be represented as edges in the graph.
5//!
6//! # Design (, Appendix A2)
7//!
8//! The enumeration covers:
9//! - **Structural**: Defines, Contains
10//! - **References**: Calls, References, Imports, Exports, `TypeOf`
11//! - **OOP**: Inherits, Implements
12//! - **Cross-language**: FFI, HTTP, gRPC, WebAssembly, DB queries
13//! - **Extended**: `MessageQueue`, WebSocket, GraphQL, `ProcessExec`, `FileIpc`
14
15use std::fmt;
16
17use serde::{Deserialize, Serialize};
18
19use super::super::string::StringId;
20
21/// Context for `TypeOf` edges (parameter, return, field, variable).
22///
23/// Indicates where a type reference appears in the code structure.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum TypeOfContext {
27    /// Function/method parameter
28    Parameter,
29    /// Function/method return value
30    Return,
31    /// Struct/class field
32    Field,
33    /// Variable declaration
34    Variable,
35    /// Type parameter (generics)
36    TypeParameter,
37    /// Type constraint
38    Constraint,
39}
40
41/// FFI calling convention.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44#[derive(Default)]
45pub enum FfiConvention {
46    /// Standard C calling convention
47    #[default]
48    C,
49    /// cdecl calling convention
50    Cdecl,
51    /// stdcall calling convention (Windows)
52    Stdcall,
53    /// fastcall calling convention
54    Fastcall,
55    /// System default calling convention
56    System,
57}
58
59/// HTTP method for HTTP request edges.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
61#[serde(rename_all = "UPPERCASE")]
62#[derive(Default)]
63pub enum HttpMethod {
64    /// GET request
65    #[default]
66    Get,
67    /// POST request
68    Post,
69    /// PUT request
70    Put,
71    /// DELETE request
72    Delete,
73    /// PATCH request
74    Patch,
75    /// HEAD request
76    Head,
77    /// OPTIONS request
78    Options,
79    /// ALL methods (wildcard — matches any HTTP method)
80    All,
81}
82
83impl HttpMethod {
84    /// Returns the HTTP method as a string.
85    #[must_use]
86    pub const fn as_str(self) -> &'static str {
87        match self {
88            Self::Get => "GET",
89            Self::Post => "POST",
90            Self::Put => "PUT",
91            Self::Delete => "DELETE",
92            Self::Patch => "PATCH",
93            Self::Head => "HEAD",
94            Self::Options => "OPTIONS",
95            Self::All => "ALL",
96        }
97    }
98}
99
100/// Database query type for DB query edges.
101#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
102#[serde(rename_all = "snake_case")]
103#[derive(Default)]
104pub enum DbQueryType {
105    /// SELECT query
106    #[default]
107    Select,
108    /// INSERT query
109    Insert,
110    /// UPDATE query
111    Update,
112    /// DELETE query
113    Delete,
114    /// EXECUTE stored procedure/function
115    Execute,
116}
117
118/// Database table write operation (SQL).
119#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
120#[serde(rename_all = "snake_case")]
121pub enum TableWriteOp {
122    /// INSERT operation
123    Insert,
124    /// UPDATE operation
125    Update,
126    /// DELETE operation
127    Delete,
128}
129
130/// Export kind for distinguishing re-exports from declarations.
131#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
132#[serde(rename_all = "snake_case")]
133#[derive(Default)]
134pub enum ExportKind {
135    /// Direct export of a symbol
136    #[default]
137    Direct,
138    /// Re-export from another module
139    Reexport,
140    /// Default export (JavaScript/TypeScript)
141    Default,
142    /// Namespace export (export *)
143    Namespace,
144}
145
146/// Message queue protocol for async communication edges.
147#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
148#[serde(rename_all = "snake_case")]
149#[derive(Default)]
150pub enum MqProtocol {
151    /// Apache Kafka
152    #[default]
153    Kafka,
154    /// AWS SQS
155    Sqs,
156    /// `RabbitMQ` / AMQP
157    RabbitMq,
158    /// NATS messaging
159    Nats,
160    /// Redis Pub/Sub
161    Redis,
162    /// Other protocol (identified by `StringId`)
163    Other(StringId),
164}
165
166/// Kind of lifetime constraint relationship (Rust-specific).
167///
168/// Models the various ways lifetimes can constrain other lifetimes or types.
169#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
170#[serde(rename_all = "snake_case")]
171#[derive(Default)]
172pub enum LifetimeConstraintKind {
173    /// `'a: 'b` - lifetime 'a outlives 'b
174    #[default]
175    Outlives,
176    /// `T: 'a` - type T is bounded by lifetime 'a
177    TypeBound,
178    /// `&'a T` - reference with explicit lifetime
179    Reference,
180    /// `'static` bound
181    Static,
182    /// Higher-ranked trait bound: `for<'a> T: Trait<'a>`
183    HigherRanked,
184    /// Trait object bound: `dyn Trait + 'a`
185    TraitObject,
186    /// impl Trait bound: `impl Trait + 'a`
187    ImplTrait,
188    /// Elided lifetime (inferred by compiler, requires RA)
189    Elided,
190}
191
192/// Kind of macro expansion (Rust-specific).
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
194#[serde(rename_all = "snake_case")]
195#[derive(Default)]
196pub enum MacroExpansionKind {
197    /// Derive macro (`#[derive(...)]`)
198    Derive,
199    /// Attribute macro (`#[proc_macro_attribute]`)
200    Attribute,
201    /// Declarative macro (`macro_rules!`)
202    #[default]
203    Declarative,
204    /// Function-like macro
205    Function,
206    /// Conditional compilation gate (`#[cfg(...)]`, `#[cfg_attr(...)]`)
207    CfgGate,
208}
209
210/// Enumeration of edge relationship types in the graph.
211///
212/// Each variant represents a distinct kind of relationship between nodes.
213/// The categorization is language-agnostic to support cross-language analysis.
214///
215/// Note: Uses default externally-tagged enum representation for serialization compatibility.
216/// JSON output will be `{"calls": {"argument_count": 0, "is_async": false}}` rather than
217/// `{"type": "calls", "argument_count": 0, "is_async": false}`.
218#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
219#[serde(rename_all = "snake_case")]
220pub enum EdgeKind {
221    // ==================== Structural ====================
222    /// A symbol defines another (e.g., module defines function).
223    Defines,
224
225    /// A container contains another (e.g., class contains method).
226    Contains,
227
228    // ==================== References ====================
229    /// A function/method calls another.
230    Calls {
231        /// Number of arguments in the call (0-255)
232        argument_count: u8,
233        /// Whether the call expression is directly awaited (uses `.await`).
234        ///
235        /// This indicates an *awaited call site*, not merely "inside an async function".
236        is_async: bool,
237    },
238
239    /// A symbol references another (read access).
240    References,
241
242    /// An import statement brings in a symbol.
243    Imports {
244        /// Optional alias for the import (e.g., `import { foo as bar }`)
245        alias: Option<StringId>,
246        /// Whether this is a wildcard import (e.g., `import *`)
247        is_wildcard: bool,
248    },
249
250    /// An export statement exposes a symbol.
251    Exports {
252        /// The kind of export (direct, re-export, default, namespace)
253        kind: ExportKind,
254        /// Optional alias for the export (e.g., `export { foo as bar }`)
255        alias: Option<StringId>,
256    },
257
258    /// A type reference with optional context metadata.
259    ///
260    /// Represents relationships like:
261    /// - Function parameter types (`context = Parameter`)
262    /// - Function return types (`context = Return`)
263    /// - Variable types (`context = Variable`)
264    /// - Struct field types (`context = Field`)
265    ///
266    /// The `context`, `index`, and `name` fields provide semantic information
267    /// about where and how the type reference appears.
268    TypeOf {
269        /// Context where this type reference appears
270        context: Option<TypeOfContext>,
271        /// Position/index (for parameters, returns, fields)
272        index: Option<u16>,
273        /// Name (for parameters, returns, fields, variables)
274        name: Option<StringId>,
275    },
276
277    // ==================== OOP ====================
278    /// A class inherits from another (extends).
279    Inherits,
280
281    /// A class/struct implements an interface/trait.
282    Implements,
283
284    // ==================== Rust-Specific ====================
285    /// Lifetime constraint relationship.
286    ///
287    /// Models Rust lifetime bounds. Source and target are `NodeKind::Lifetime`
288    /// or `NodeKind::Type` nodes (for type bounds like `T: 'a`).
289    ///
290    /// Query semantics: NOT included in `callers/callees` results.
291    /// Use dedicated query: `--lifetime-constraints`
292    LifetimeConstraint {
293        /// The kind of lifetime constraint
294        constraint_kind: LifetimeConstraintKind,
295    },
296
297    /// Trait method binding (call site -> impl method).
298    ///
299    /// Represents the resolution of a trait method call to a concrete
300    /// implementation. This is distinct from `Calls` because it involves
301    /// trait method resolution logic.
302    ///
303    /// Query semantics: NOT included in `callers` by default.
304    /// Use `--include-trait-bindings` flag or dedicated query.
305    TraitMethodBinding {
306        /// The trait providing the method
307        trait_name: StringId,
308        /// The implementing type
309        impl_type: StringId,
310        /// Whether this binding is ambiguous (multiple impls match)
311        is_ambiguous: bool,
312    },
313
314    /// Macro expansion relationship.
315    ///
316    /// Represents the expansion of a macro invocation to its generated code.
317    /// Only available when macro expansion is enabled (security opt-in).
318    ///
319    /// Query semantics: Included in `callees` when expansion enabled.
320    MacroExpansion {
321        /// The kind of macro expansion
322        expansion_kind: MacroExpansionKind,
323        /// Whether the expansion has been verified against source
324        is_verified: bool,
325    },
326
327    // ==================== Cross-language / Cross-service ====================
328    /// Foreign function interface call.
329    FfiCall {
330        /// The calling convention used
331        convention: FfiConvention,
332    },
333
334    /// HTTP request to an endpoint.
335    HttpRequest {
336        /// The HTTP method
337        method: HttpMethod,
338        /// Optional URL pattern
339        url: Option<StringId>,
340    },
341
342    /// gRPC service call.
343    GrpcCall {
344        /// Service name
345        service: StringId,
346        /// Method name
347        method: StringId,
348    },
349
350    /// WebAssembly function call.
351    WebAssemblyCall,
352
353    /// Database query execution.
354    DbQuery {
355        /// Query type
356        query_type: DbQueryType,
357        /// Optional table/collection name
358        table: Option<StringId>,
359    },
360
361    /// Database table read operation (SQL).
362    TableRead {
363        /// Name of the table being read
364        table_name: StringId,
365        /// Optional schema/database name
366        schema: Option<StringId>,
367    },
368
369    /// Database table write operation (SQL).
370    TableWrite {
371        /// Name of the table being written
372        table_name: StringId,
373        /// Optional schema/database name
374        schema: Option<StringId>,
375        /// Type of write operation (INSERT/UPDATE/DELETE)
376        operation: TableWriteOp,
377    },
378
379    /// Database trigger relationship (SQL).
380    TriggeredBy {
381        /// Name of the trigger
382        trigger_name: StringId,
383        /// Optional schema/database name
384        schema: Option<StringId>,
385    },
386
387    // ==================== Extended ====================
388    /// Message queue publish/subscribe.
389    MessageQueue {
390        /// Protocol used
391        protocol: MqProtocol,
392        /// Optional topic/queue name
393        topic: Option<StringId>,
394    },
395
396    /// WebSocket event communication.
397    WebSocket {
398        /// Optional event name
399        event: Option<StringId>,
400    },
401
402    /// GraphQL operation (query/mutation/subscription).
403    GraphQLOperation {
404        /// Operation name
405        operation: StringId,
406    },
407
408    /// Process execution (spawn, exec).
409    ProcessExec {
410        /// Command being executed
411        command: StringId,
412    },
413
414    /// File-based IPC (pipes, shared memory, temp files).
415    FileIpc {
416        /// Optional path pattern
417        path_pattern: Option<StringId>,
418    },
419
420    // ==================== Extensibility ====================
421    /// Generic protocol call for extensibility.
422    ProtocolCall {
423        /// Protocol identifier
424        protocol: StringId,
425        /// Optional JSON-encoded metadata
426        metadata: Option<StringId>,
427    },
428
429    // ==================== JVM Classpath (Track C) ====================
430    /// Generic type bound (e.g., `T extends Comparable<T>`).
431    GenericBound,
432
433    /// Symbol annotated with annotation type.
434    AnnotatedWith,
435
436    /// Annotation parameter binding (annotation -> element value).
437    AnnotationParam,
438
439    /// Lambda captures a method reference target.
440    LambdaCaptures,
441
442    /// Java module exports a package.
443    ModuleExports,
444
445    /// Java module requires another module.
446    ModuleRequires,
447
448    /// Java module opens a package for reflection.
449    ModuleOpens,
450
451    /// Java module provides a service implementation.
452    ModuleProvides,
453
454    /// Generic type argument (e.g., `String` in `List<String>`).
455    TypeArgument,
456
457    /// Kotlin extension function receiver type.
458    ExtensionReceiver,
459
460    /// Kotlin companion object relationship.
461    CompanionOf,
462
463    /// Kotlin sealed class permits a subclass.
464    SealedPermit,
465}
466
467impl EdgeKind {
468    /// Returns `true` if this edge represents a function call relationship.
469    #[inline]
470    #[must_use]
471    pub const fn is_call(&self) -> bool {
472        matches!(
473            self,
474            Self::Calls { .. }
475                | Self::FfiCall { .. }
476                | Self::HttpRequest { .. }
477                | Self::GrpcCall { .. }
478                | Self::WebAssemblyCall
479        )
480    }
481
482    /// Returns `true` if this edge represents a structural relationship.
483    #[inline]
484    #[must_use]
485    pub const fn is_structural(&self) -> bool {
486        matches!(self, Self::Defines | Self::Contains)
487    }
488
489    /// Returns `true` if this edge represents a type relationship.
490    #[inline]
491    #[must_use]
492    pub const fn is_type_relation(&self) -> bool {
493        matches!(
494            self,
495            Self::Inherits
496                | Self::Implements
497                | Self::TypeOf { .. }
498                | Self::GenericBound
499                | Self::TypeArgument
500                | Self::ExtensionReceiver
501                | Self::SealedPermit
502        )
503    }
504
505    /// Returns `true` if this edge represents a cross-language/service boundary.
506    #[inline]
507    #[must_use]
508    pub const fn is_cross_boundary(&self) -> bool {
509        matches!(
510            self,
511            Self::FfiCall { .. }
512                | Self::HttpRequest { .. }
513                | Self::GrpcCall { .. }
514                | Self::WebAssemblyCall
515                | Self::DbQuery { .. }
516                | Self::TableRead { .. }
517                | Self::TableWrite { .. }
518                | Self::TriggeredBy { .. }
519                | Self::MessageQueue { .. }
520                | Self::WebSocket { .. }
521                | Self::GraphQLOperation { .. }
522                | Self::ProcessExec { .. }
523                | Self::FileIpc { .. }
524                | Self::ProtocolCall { .. }
525        )
526    }
527
528    /// Returns `true` if this is an async/message-based relationship.
529    #[inline]
530    #[must_use]
531    pub const fn is_async(&self) -> bool {
532        matches!(
533            self,
534            Self::MessageQueue { .. } | Self::WebSocket { .. } | Self::GraphQLOperation { .. }
535        )
536    }
537
538    /// Returns `true` if this is a Rust-specific edge kind.
539    ///
540    /// These edges are produced by the Rust language plugin and have
541    /// specialized query semantics.
542    #[inline]
543    #[must_use]
544    pub const fn is_rust_specific(&self) -> bool {
545        matches!(
546            self,
547            Self::LifetimeConstraint { .. }
548                | Self::TraitMethodBinding { .. }
549                | Self::MacroExpansion { .. }
550        )
551    }
552
553    /// Returns `true` if this is a lifetime constraint edge.
554    #[inline]
555    #[must_use]
556    pub const fn is_lifetime_constraint(&self) -> bool {
557        matches!(self, Self::LifetimeConstraint { .. })
558    }
559
560    /// Returns `true` if this is a trait method binding edge.
561    #[inline]
562    #[must_use]
563    pub const fn is_trait_method_binding(&self) -> bool {
564        matches!(self, Self::TraitMethodBinding { .. })
565    }
566
567    /// Returns `true` if this is a macro expansion edge.
568    #[inline]
569    #[must_use]
570    pub const fn is_macro_expansion(&self) -> bool {
571        matches!(self, Self::MacroExpansion { .. })
572    }
573
574    /// Returns the canonical tag name for this edge kind.
575    #[must_use]
576    pub const fn tag(&self) -> &'static str {
577        match self {
578            Self::Defines => "defines",
579            Self::Contains => "contains",
580            Self::Calls { .. } => "calls",
581            Self::References => "references",
582            Self::Imports { .. } => "imports",
583            Self::Exports { .. } => "exports",
584            Self::TypeOf { .. } => "type_of",
585            Self::Inherits => "inherits",
586            Self::Implements => "implements",
587            Self::LifetimeConstraint { .. } => "lifetime_constraint",
588            Self::TraitMethodBinding { .. } => "trait_method_binding",
589            Self::MacroExpansion { .. } => "macro_expansion",
590            Self::FfiCall { .. } => "ffi_call",
591            Self::HttpRequest { .. } => "http_request",
592            Self::GrpcCall { .. } => "grpc_call",
593            Self::WebAssemblyCall => "web_assembly_call",
594            Self::DbQuery { .. } => "db_query",
595            Self::TableRead { .. } => "table_read",
596            Self::TableWrite { .. } => "table_write",
597            Self::TriggeredBy { .. } => "triggered_by",
598            Self::MessageQueue { .. } => "message_queue",
599            Self::WebSocket { .. } => "web_socket",
600            Self::GraphQLOperation { .. } => "graphql_operation",
601            Self::ProcessExec { .. } => "process_exec",
602            Self::FileIpc { .. } => "file_ipc",
603            Self::ProtocolCall { .. } => "protocol_call",
604            Self::GenericBound => "generic_bound",
605            Self::AnnotatedWith => "annotated_with",
606            Self::AnnotationParam => "annotation_param",
607            Self::LambdaCaptures => "lambda_captures",
608            Self::ModuleExports => "module_exports",
609            Self::ModuleRequires => "module_requires",
610            Self::ModuleOpens => "module_opens",
611            Self::ModuleProvides => "module_provides",
612            Self::TypeArgument => "type_argument",
613            Self::ExtensionReceiver => "extension_receiver",
614            Self::CompanionOf => "companion_of",
615            Self::SealedPermit => "sealed_permit",
616        }
617    }
618
619    /// Returns an estimated byte size for this edge kind variant.
620    ///
621    /// Used for byte-level admission control in the delta buffer.
622    /// Estimates are conservative approximations based on variant data.
623    #[must_use]
624    pub const fn estimated_size(&self) -> usize {
625        // Base enum discriminant: 1 byte
626        // StringId: 4 bytes each
627        // Option<StringId>: 5 bytes (1 discriminant + 4 payload)
628        // bool: 1 byte, u8: 1 byte, ExportKind: 1 byte
629        match self {
630            // Unit variants: just discriminant
631            Self::Defines
632            | Self::Contains
633            | Self::References
634            | Self::Inherits
635            | Self::Implements
636            | Self::WebAssemblyCall
637            | Self::GenericBound
638            | Self::AnnotatedWith
639            | Self::AnnotationParam
640            | Self::LambdaCaptures
641            | Self::ModuleExports
642            | Self::ModuleRequires
643            | Self::ModuleOpens
644            | Self::ModuleProvides
645            | Self::TypeArgument
646            | Self::ExtensionReceiver
647            | Self::CompanionOf
648            | Self::SealedPermit => 1,
649
650            // u8 + bool: 1 + 1 + 1
651            // MacroExpansionKind + bool: 1 + 1
652            Self::Calls { .. } | Self::MacroExpansion { .. } => 3,
653
654            // Option<StringId> + bool: 5 + 1 + 1 (imports/exports)
655            // DbQueryType + Option<StringId>: 1 + 5
656            Self::Imports { .. } | Self::Exports { .. } | Self::DbQuery { .. } => 7,
657
658            // FfiConvention: 1 byte
659            // LifetimeConstraintKind: 1 byte
660            Self::FfiCall { .. } | Self::LifetimeConstraint { .. } => 2,
661
662            // StringId + StringId + bool: 4 + 4 + 1
663            // StringId + Option<StringId>: 4 + 5
664            Self::TraitMethodBinding { .. }
665            | Self::TableRead { .. }
666            | Self::TriggeredBy { .. }
667            | Self::ProtocolCall { .. } => 10,
668
669            // HttpMethod + Option<StringId>: 1 + 5
670            // Option<StringId>: 5 (websocket/file IPC)
671            Self::HttpRequest { .. } | Self::WebSocket { .. } | Self::FileIpc { .. } => 6,
672
673            // Two StringIds: 4 + 4
674            Self::GrpcCall { .. } => 9,
675
676            // StringId + Option<StringId> + TableWriteOp: 4 + 5 + 1
677            // Option<TypeOfContext> + Option<u16> + Option<StringId>: 2 + 3 + 5
678            Self::TableWrite { .. } | Self::MessageQueue { .. } | Self::TypeOf { .. } => 11,
679
680            // StringId: 4
681            Self::GraphQLOperation { .. } | Self::ProcessExec { .. } => 5,
682        }
683    }
684}
685
686impl fmt::Display for EdgeKind {
687    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
688        f.write_str(self.tag())
689    }
690}
691
692impl Default for EdgeKind {
693    /// Returns `EdgeKind::Calls` as the default (most common edge type).
694    fn default() -> Self {
695        Self::Calls {
696            argument_count: 0,
697            is_async: false,
698        }
699    }
700}
701
702#[cfg(test)]
703mod tests {
704    use super::*;
705
706    /// Helper to create a default Calls variant for tests.
707    fn calls() -> EdgeKind {
708        EdgeKind::Calls {
709            argument_count: 0,
710            is_async: false,
711        }
712    }
713
714    /// Helper to create a default Imports variant for tests.
715    fn imports() -> EdgeKind {
716        EdgeKind::Imports {
717            alias: None,
718            is_wildcard: false,
719        }
720    }
721
722    /// Helper to create a default Exports variant for tests.
723    fn exports() -> EdgeKind {
724        EdgeKind::Exports {
725            kind: ExportKind::Direct,
726            alias: None,
727        }
728    }
729
730    #[test]
731    fn test_edge_kind_tag() {
732        assert_eq!(calls().tag(), "calls");
733        assert_eq!(imports().tag(), "imports");
734        assert_eq!(exports().tag(), "exports");
735        assert_eq!(EdgeKind::Defines.tag(), "defines");
736        assert_eq!(
737            EdgeKind::HttpRequest {
738                method: HttpMethod::Get,
739                url: None
740            }
741            .tag(),
742            "http_request"
743        );
744    }
745
746    #[test]
747    fn test_edge_kind_display() {
748        assert_eq!(format!("{}", calls()), "calls");
749        assert_eq!(format!("{}", imports()), "imports");
750        assert_eq!(format!("{}", exports()), "exports");
751        assert_eq!(format!("{}", EdgeKind::Inherits), "inherits");
752    }
753
754    #[test]
755    fn test_is_call() {
756        assert!(calls().is_call());
757        assert!(
758            EdgeKind::Calls {
759                argument_count: 5,
760                is_async: true
761            }
762            .is_call()
763        );
764        assert!(
765            EdgeKind::FfiCall {
766                convention: FfiConvention::C
767            }
768            .is_call()
769        );
770        assert!(
771            EdgeKind::HttpRequest {
772                method: HttpMethod::Post,
773                url: None
774            }
775            .is_call()
776        );
777        assert!(!EdgeKind::Defines.is_call());
778        assert!(!EdgeKind::Inherits.is_call());
779        assert!(!imports().is_call());
780        assert!(!exports().is_call());
781    }
782
783    #[test]
784    fn test_is_structural() {
785        assert!(EdgeKind::Defines.is_structural());
786        assert!(EdgeKind::Contains.is_structural());
787        assert!(!calls().is_structural());
788        assert!(!imports().is_structural());
789        assert!(!exports().is_structural());
790    }
791
792    #[test]
793    fn test_is_type_relation() {
794        assert!(EdgeKind::Inherits.is_type_relation());
795        assert!(EdgeKind::Implements.is_type_relation());
796        assert!(
797            EdgeKind::TypeOf {
798                context: None,
799                index: None,
800                name: None,
801            }
802            .is_type_relation()
803        );
804        assert!(!calls().is_type_relation());
805    }
806
807    #[test]
808    fn test_is_cross_boundary() {
809        assert!(
810            EdgeKind::FfiCall {
811                convention: FfiConvention::C
812            }
813            .is_cross_boundary()
814        );
815        assert!(
816            EdgeKind::HttpRequest {
817                method: HttpMethod::Get,
818                url: None
819            }
820            .is_cross_boundary()
821        );
822        assert!(
823            EdgeKind::GrpcCall {
824                service: StringId::INVALID,
825                method: StringId::INVALID
826            }
827            .is_cross_boundary()
828        );
829        assert!(!calls().is_cross_boundary());
830        assert!(!imports().is_cross_boundary());
831        assert!(!exports().is_cross_boundary());
832    }
833
834    #[test]
835    fn test_is_async() {
836        assert!(
837            EdgeKind::MessageQueue {
838                protocol: MqProtocol::Kafka,
839                topic: None
840            }
841            .is_async()
842        );
843        assert!(EdgeKind::WebSocket { event: None }.is_async());
844        assert!(!calls().is_async());
845        // Note: EdgeKind::Calls with is_async: true still returns false from is_async()
846        // because is_async() refers to async communication patterns, not async function calls
847        assert!(
848            !EdgeKind::Calls {
849                argument_count: 0,
850                is_async: true
851            }
852            .is_async()
853        );
854    }
855
856    #[test]
857    fn test_default() {
858        assert_eq!(EdgeKind::default(), calls());
859        assert_eq!(HttpMethod::default(), HttpMethod::Get);
860        assert_eq!(FfiConvention::default(), FfiConvention::C);
861        assert_eq!(DbQueryType::default(), DbQueryType::Select);
862        assert_eq!(ExportKind::default(), ExportKind::Direct);
863    }
864
865    #[test]
866    fn test_http_method_as_str() {
867        assert_eq!(HttpMethod::Get.as_str(), "GET");
868        assert_eq!(HttpMethod::Post.as_str(), "POST");
869        assert_eq!(HttpMethod::Delete.as_str(), "DELETE");
870        assert_eq!(HttpMethod::All.as_str(), "ALL");
871    }
872
873    #[test]
874    fn test_calls_with_metadata() {
875        let sync_call = EdgeKind::Calls {
876            argument_count: 3,
877            is_async: false,
878        };
879        let async_call = EdgeKind::Calls {
880            argument_count: 0,
881            is_async: true,
882        };
883        assert_eq!(sync_call.tag(), "calls");
884        assert_eq!(async_call.tag(), "calls");
885        assert!(sync_call.is_call());
886        assert!(async_call.is_call());
887        assert_ne!(sync_call, async_call);
888    }
889
890    #[test]
891    fn test_imports_with_metadata() {
892        let simple = imports();
893        let aliased = EdgeKind::Imports {
894            alias: Some(StringId::new(42)),
895            is_wildcard: false,
896        };
897        let wildcard = EdgeKind::Imports {
898            alias: None,
899            is_wildcard: true,
900        };
901
902        assert_eq!(simple.tag(), "imports");
903        assert_eq!(aliased.tag(), "imports");
904        assert_eq!(wildcard.tag(), "imports");
905        assert_ne!(simple, aliased);
906        assert_ne!(simple, wildcard);
907    }
908
909    #[test]
910    fn test_exports_with_metadata() {
911        let direct = exports();
912        let reexport = EdgeKind::Exports {
913            kind: ExportKind::Reexport,
914            alias: None,
915        };
916        let default_export = EdgeKind::Exports {
917            kind: ExportKind::Default,
918            alias: None,
919        };
920        let namespace = EdgeKind::Exports {
921            kind: ExportKind::Namespace,
922            alias: Some(StringId::new(1)),
923        };
924
925        assert_eq!(direct.tag(), "exports");
926        assert_eq!(reexport.tag(), "exports");
927        assert_eq!(default_export.tag(), "exports");
928        assert_eq!(namespace.tag(), "exports");
929        assert_ne!(direct, reexport);
930        assert_ne!(direct, default_export);
931    }
932
933    #[test]
934    fn test_serde_calls_imports_exports() {
935        // Calls with metadata
936        let calls = EdgeKind::Calls {
937            argument_count: 5,
938            is_async: true,
939        };
940        let json = serde_json::to_string(&calls).unwrap();
941        let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
942        assert_eq!(calls, deserialized);
943        assert!(json.contains("\"calls\""));
944        assert!(json.contains("\"argument_count\":5"));
945        assert!(json.contains("\"is_async\":true"));
946
947        // Imports with alias
948        let imports = EdgeKind::Imports {
949            alias: Some(StringId::new(10)),
950            is_wildcard: false,
951        };
952        let json = serde_json::to_string(&imports).unwrap();
953        let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
954        assert_eq!(imports, deserialized);
955
956        // Exports with kind
957        let exports = EdgeKind::Exports {
958            kind: ExportKind::Reexport,
959            alias: None,
960        };
961        let json = serde_json::to_string(&exports).unwrap();
962        let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
963        assert_eq!(exports, deserialized);
964    }
965
966    #[test]
967    fn test_serde_complex_variants() {
968        // HttpRequest with fields
969        let http = EdgeKind::HttpRequest {
970            method: HttpMethod::Post,
971            url: None,
972        };
973        let json = serde_json::to_string(&http).unwrap();
974        let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
975        assert_eq!(http, deserialized);
976
977        // GrpcCall with StringIds
978        let grpc = EdgeKind::GrpcCall {
979            service: StringId::new(1),
980            method: StringId::new(2),
981        };
982        let json = serde_json::to_string(&grpc).unwrap();
983        let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
984        assert_eq!(grpc, deserialized);
985    }
986
987    #[test]
988    fn test_postcard_roundtrip_simple_enums() {
989        // Test postcard roundtrip for component enums used by EdgeKind.
990
991        // FfiConvention
992        for conv in [
993            FfiConvention::C,
994            FfiConvention::Cdecl,
995            FfiConvention::Stdcall,
996        ] {
997            let bytes = postcard::to_allocvec(&conv).unwrap();
998            let deserialized: FfiConvention = postcard::from_bytes(&bytes).unwrap();
999            assert_eq!(conv, deserialized);
1000        }
1001
1002        // HttpMethod
1003        for method in [
1004            HttpMethod::Get,
1005            HttpMethod::Post,
1006            HttpMethod::Delete,
1007            HttpMethod::All,
1008        ] {
1009            let bytes = postcard::to_allocvec(&method).unwrap();
1010            let deserialized: HttpMethod = postcard::from_bytes(&bytes).unwrap();
1011            assert_eq!(method, deserialized);
1012        }
1013
1014        // DbQueryType
1015        for query in [
1016            DbQueryType::Select,
1017            DbQueryType::Insert,
1018            DbQueryType::Update,
1019        ] {
1020            let bytes = postcard::to_allocvec(&query).unwrap();
1021            let deserialized: DbQueryType = postcard::from_bytes(&bytes).unwrap();
1022            assert_eq!(query, deserialized);
1023        }
1024
1025        // ExportKind
1026        for kind in [
1027            ExportKind::Direct,
1028            ExportKind::Reexport,
1029            ExportKind::Default,
1030            ExportKind::Namespace,
1031        ] {
1032            let bytes = postcard::to_allocvec(&kind).unwrap();
1033            let deserialized: ExportKind = postcard::from_bytes(&bytes).unwrap();
1034            assert_eq!(kind, deserialized);
1035        }
1036    }
1037
1038    #[test]
1039    fn test_edge_kind_json_compatibility() {
1040        // EdgeKind is designed for JSON serialization (MCP export).
1041        // Binary persistence in Phase 6 will use a custom format.
1042        let kinds = [
1043            calls(),
1044            imports(),
1045            exports(),
1046            EdgeKind::Defines,
1047            EdgeKind::HttpRequest {
1048                method: HttpMethod::Get,
1049                url: None,
1050            },
1051            EdgeKind::MessageQueue {
1052                protocol: MqProtocol::Kafka,
1053                topic: Some(StringId::new(1)),
1054            },
1055        ];
1056
1057        for kind in &kinds {
1058            // JSON roundtrip should work
1059            let json = serde_json::to_string(kind).unwrap();
1060            let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
1061            assert_eq!(*kind, deserialized);
1062
1063            // Postcard roundtrip should also work (required for graph persistence)
1064            let bytes = postcard::to_allocvec(kind).unwrap();
1065            let from_postcard: EdgeKind = postcard::from_bytes(&bytes).unwrap();
1066            assert_eq!(*kind, from_postcard);
1067        }
1068    }
1069
1070    #[test]
1071    fn test_hash() {
1072        use std::collections::HashSet;
1073
1074        let mut set = HashSet::new();
1075        set.insert(calls());
1076        set.insert(imports());
1077        set.insert(exports());
1078        set.insert(EdgeKind::Defines);
1079        set.insert(EdgeKind::HttpRequest {
1080            method: HttpMethod::Get,
1081            url: None,
1082        });
1083
1084        assert!(set.contains(&calls()));
1085        assert!(set.contains(&imports()));
1086        assert!(set.contains(&exports()));
1087        assert!(!set.contains(&EdgeKind::Inherits));
1088        assert_eq!(set.len(), 5);
1089    }
1090
1091    #[test]
1092    fn test_ffi_convention_variants() {
1093        let conventions = [
1094            FfiConvention::C,
1095            FfiConvention::Cdecl,
1096            FfiConvention::Stdcall,
1097            FfiConvention::Fastcall,
1098            FfiConvention::System,
1099        ];
1100
1101        for conv in conventions {
1102            let edge = EdgeKind::FfiCall { convention: conv };
1103            assert!(edge.is_call());
1104            assert!(edge.is_cross_boundary());
1105        }
1106    }
1107
1108    #[test]
1109    fn test_mq_protocol_variants() {
1110        let protocols = [
1111            MqProtocol::Kafka,
1112            MqProtocol::Sqs,
1113            MqProtocol::RabbitMq,
1114            MqProtocol::Nats,
1115            MqProtocol::Redis,
1116            MqProtocol::Other(StringId::new(1)),
1117        ];
1118
1119        for proto in protocols {
1120            let edge = EdgeKind::MessageQueue {
1121                protocol: proto.clone(),
1122                topic: None,
1123            };
1124            assert!(edge.is_async());
1125            assert!(edge.is_cross_boundary());
1126        }
1127    }
1128
1129    #[test]
1130    fn test_export_kind_variants() {
1131        let kinds = [
1132            ExportKind::Direct,
1133            ExportKind::Reexport,
1134            ExportKind::Default,
1135            ExportKind::Namespace,
1136        ];
1137
1138        for kind in kinds {
1139            let edge = EdgeKind::Exports { kind, alias: None };
1140            assert_eq!(edge.tag(), "exports");
1141            assert!(!edge.is_call());
1142            assert!(!edge.is_structural());
1143            assert!(!edge.is_cross_boundary());
1144        }
1145    }
1146
1147    #[test]
1148    fn test_estimated_size() {
1149        // Unit variants
1150        assert_eq!(EdgeKind::Defines.estimated_size(), 1);
1151        assert_eq!(EdgeKind::Contains.estimated_size(), 1);
1152        assert_eq!(EdgeKind::References.estimated_size(), 1);
1153
1154        // Calls: u8 + bool = 3
1155        assert_eq!(calls().estimated_size(), 3);
1156
1157        // Imports: Option<StringId> + bool = 7
1158        assert_eq!(imports().estimated_size(), 7);
1159
1160        // Exports: ExportKind + Option<StringId> = 7
1161        assert_eq!(exports().estimated_size(), 7);
1162
1163        // Rust-specific edges
1164        assert_eq!(
1165            EdgeKind::LifetimeConstraint {
1166                constraint_kind: LifetimeConstraintKind::Outlives
1167            }
1168            .estimated_size(),
1169            2
1170        );
1171        assert_eq!(
1172            EdgeKind::MacroExpansion {
1173                expansion_kind: MacroExpansionKind::Derive,
1174                is_verified: true
1175            }
1176            .estimated_size(),
1177            3
1178        );
1179        assert_eq!(
1180            EdgeKind::TraitMethodBinding {
1181                trait_name: StringId::INVALID,
1182                impl_type: StringId::INVALID,
1183                is_ambiguous: false
1184            }
1185            .estimated_size(),
1186            10
1187        );
1188    }
1189
1190    // ==================== Rust-Specific Edge Tests ====================
1191
1192    #[test]
1193    fn test_lifetime_constraint_kind_variants() {
1194        let kinds = [
1195            LifetimeConstraintKind::Outlives,
1196            LifetimeConstraintKind::TypeBound,
1197            LifetimeConstraintKind::Reference,
1198            LifetimeConstraintKind::Static,
1199            LifetimeConstraintKind::HigherRanked,
1200            LifetimeConstraintKind::TraitObject,
1201            LifetimeConstraintKind::ImplTrait,
1202            LifetimeConstraintKind::Elided,
1203        ];
1204
1205        for constraint_kind in kinds {
1206            let edge = EdgeKind::LifetimeConstraint { constraint_kind };
1207            assert!(edge.is_rust_specific());
1208            assert!(edge.is_lifetime_constraint());
1209            assert!(!edge.is_call());
1210            assert!(!edge.is_structural());
1211            assert_eq!(edge.tag(), "lifetime_constraint");
1212        }
1213    }
1214
1215    #[test]
1216    fn test_macro_expansion_kind_variants() {
1217        let kinds = [
1218            MacroExpansionKind::Derive,
1219            MacroExpansionKind::Attribute,
1220            MacroExpansionKind::Declarative,
1221            MacroExpansionKind::Function,
1222            MacroExpansionKind::CfgGate,
1223        ];
1224
1225        for expansion_kind in kinds {
1226            let edge = EdgeKind::MacroExpansion {
1227                expansion_kind,
1228                is_verified: true,
1229            };
1230            assert!(edge.is_rust_specific());
1231            assert!(edge.is_macro_expansion());
1232            assert!(!edge.is_call());
1233            assert_eq!(edge.tag(), "macro_expansion");
1234        }
1235    }
1236
1237    #[test]
1238    fn test_trait_method_binding() {
1239        let edge = EdgeKind::TraitMethodBinding {
1240            trait_name: StringId::new(1),
1241            impl_type: StringId::new(2),
1242            is_ambiguous: false,
1243        };
1244
1245        assert!(edge.is_rust_specific());
1246        assert!(edge.is_trait_method_binding());
1247        assert!(!edge.is_call());
1248        assert_eq!(edge.tag(), "trait_method_binding");
1249
1250        // Test ambiguous binding
1251        let ambiguous = EdgeKind::TraitMethodBinding {
1252            trait_name: StringId::new(1),
1253            impl_type: StringId::new(2),
1254            is_ambiguous: true,
1255        };
1256        assert!(ambiguous.is_trait_method_binding());
1257    }
1258
1259    #[test]
1260    fn test_rust_specific_edges_serde() {
1261        // LifetimeConstraint
1262        let lifetime = EdgeKind::LifetimeConstraint {
1263            constraint_kind: LifetimeConstraintKind::HigherRanked,
1264        };
1265        let json = serde_json::to_string(&lifetime).unwrap();
1266        let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
1267        assert_eq!(lifetime, deserialized);
1268
1269        // TraitMethodBinding
1270        let binding = EdgeKind::TraitMethodBinding {
1271            trait_name: StringId::new(10),
1272            impl_type: StringId::new(20),
1273            is_ambiguous: true,
1274        };
1275        let json = serde_json::to_string(&binding).unwrap();
1276        let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
1277        assert_eq!(binding, deserialized);
1278
1279        // MacroExpansion
1280        let expansion = EdgeKind::MacroExpansion {
1281            expansion_kind: MacroExpansionKind::Derive,
1282            is_verified: false,
1283        };
1284        let json = serde_json::to_string(&expansion).unwrap();
1285        let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
1286        assert_eq!(expansion, deserialized);
1287    }
1288
1289    #[test]
1290    fn test_rust_specific_edges_postcard() {
1291        let edges = [
1292            EdgeKind::LifetimeConstraint {
1293                constraint_kind: LifetimeConstraintKind::Outlives,
1294            },
1295            EdgeKind::TraitMethodBinding {
1296                trait_name: StringId::new(5),
1297                impl_type: StringId::new(6),
1298                is_ambiguous: false,
1299            },
1300            EdgeKind::MacroExpansion {
1301                expansion_kind: MacroExpansionKind::Attribute,
1302                is_verified: true,
1303            },
1304        ];
1305
1306        for edge in edges {
1307            let bytes = postcard::to_allocvec(&edge).unwrap();
1308            let deserialized: EdgeKind = postcard::from_bytes(&bytes).unwrap();
1309            assert_eq!(edge, deserialized);
1310        }
1311    }
1312
1313    #[test]
1314    fn test_lifetime_constraint_kind_defaults() {
1315        assert_eq!(
1316            LifetimeConstraintKind::default(),
1317            LifetimeConstraintKind::Outlives
1318        );
1319        assert_eq!(
1320            MacroExpansionKind::default(),
1321            MacroExpansionKind::Declarative
1322        );
1323    }
1324}