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