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
430impl EdgeKind {
431    /// Returns `true` if this edge represents a function call relationship.
432    #[inline]
433    #[must_use]
434    pub const fn is_call(&self) -> bool {
435        matches!(
436            self,
437            Self::Calls { .. }
438                | Self::FfiCall { .. }
439                | Self::HttpRequest { .. }
440                | Self::GrpcCall { .. }
441                | Self::WebAssemblyCall
442        )
443    }
444
445    /// Returns `true` if this edge represents a structural relationship.
446    #[inline]
447    #[must_use]
448    pub const fn is_structural(&self) -> bool {
449        matches!(self, Self::Defines | Self::Contains)
450    }
451
452    /// Returns `true` if this edge represents a type relationship.
453    #[inline]
454    #[must_use]
455    pub const fn is_type_relation(&self) -> bool {
456        matches!(
457            self,
458            Self::Inherits | Self::Implements | Self::TypeOf { .. }
459        )
460    }
461
462    /// Returns `true` if this edge represents a cross-language/service boundary.
463    #[inline]
464    #[must_use]
465    pub const fn is_cross_boundary(&self) -> bool {
466        matches!(
467            self,
468            Self::FfiCall { .. }
469                | Self::HttpRequest { .. }
470                | Self::GrpcCall { .. }
471                | Self::WebAssemblyCall
472                | Self::DbQuery { .. }
473                | Self::TableRead { .. }
474                | Self::TableWrite { .. }
475                | Self::TriggeredBy { .. }
476                | Self::MessageQueue { .. }
477                | Self::WebSocket { .. }
478                | Self::GraphQLOperation { .. }
479                | Self::ProcessExec { .. }
480                | Self::FileIpc { .. }
481                | Self::ProtocolCall { .. }
482        )
483    }
484
485    /// Returns `true` if this is an async/message-based relationship.
486    #[inline]
487    #[must_use]
488    pub const fn is_async(&self) -> bool {
489        matches!(
490            self,
491            Self::MessageQueue { .. } | Self::WebSocket { .. } | Self::GraphQLOperation { .. }
492        )
493    }
494
495    /// Returns `true` if this is a Rust-specific edge kind.
496    ///
497    /// These edges are produced by the Rust language plugin and have
498    /// specialized query semantics.
499    #[inline]
500    #[must_use]
501    pub const fn is_rust_specific(&self) -> bool {
502        matches!(
503            self,
504            Self::LifetimeConstraint { .. }
505                | Self::TraitMethodBinding { .. }
506                | Self::MacroExpansion { .. }
507        )
508    }
509
510    /// Returns `true` if this is a lifetime constraint edge.
511    #[inline]
512    #[must_use]
513    pub const fn is_lifetime_constraint(&self) -> bool {
514        matches!(self, Self::LifetimeConstraint { .. })
515    }
516
517    /// Returns `true` if this is a trait method binding edge.
518    #[inline]
519    #[must_use]
520    pub const fn is_trait_method_binding(&self) -> bool {
521        matches!(self, Self::TraitMethodBinding { .. })
522    }
523
524    /// Returns `true` if this is a macro expansion edge.
525    #[inline]
526    #[must_use]
527    pub const fn is_macro_expansion(&self) -> bool {
528        matches!(self, Self::MacroExpansion { .. })
529    }
530
531    /// Returns the canonical tag name for this edge kind.
532    #[must_use]
533    pub const fn tag(&self) -> &'static str {
534        match self {
535            Self::Defines => "defines",
536            Self::Contains => "contains",
537            Self::Calls { .. } => "calls",
538            Self::References => "references",
539            Self::Imports { .. } => "imports",
540            Self::Exports { .. } => "exports",
541            Self::TypeOf { .. } => "type_of",
542            Self::Inherits => "inherits",
543            Self::Implements => "implements",
544            Self::LifetimeConstraint { .. } => "lifetime_constraint",
545            Self::TraitMethodBinding { .. } => "trait_method_binding",
546            Self::MacroExpansion { .. } => "macro_expansion",
547            Self::FfiCall { .. } => "ffi_call",
548            Self::HttpRequest { .. } => "http_request",
549            Self::GrpcCall { .. } => "grpc_call",
550            Self::WebAssemblyCall => "web_assembly_call",
551            Self::DbQuery { .. } => "db_query",
552            Self::TableRead { .. } => "table_read",
553            Self::TableWrite { .. } => "table_write",
554            Self::TriggeredBy { .. } => "triggered_by",
555            Self::MessageQueue { .. } => "message_queue",
556            Self::WebSocket { .. } => "web_socket",
557            Self::GraphQLOperation { .. } => "graphql_operation",
558            Self::ProcessExec { .. } => "process_exec",
559            Self::FileIpc { .. } => "file_ipc",
560            Self::ProtocolCall { .. } => "protocol_call",
561        }
562    }
563
564    /// Returns an estimated byte size for this edge kind variant.
565    ///
566    /// Used for byte-level admission control in the delta buffer.
567    /// Estimates are conservative approximations based on variant data.
568    #[must_use]
569    pub const fn estimated_size(&self) -> usize {
570        // Base enum discriminant: 1 byte
571        // StringId: 4 bytes each
572        // Option<StringId>: 5 bytes (1 discriminant + 4 payload)
573        // bool: 1 byte, u8: 1 byte, ExportKind: 1 byte
574        match self {
575            // Unit variants: just discriminant
576            Self::Defines
577            | Self::Contains
578            | Self::References
579            | Self::Inherits
580            | Self::Implements
581            | Self::WebAssemblyCall => 1,
582
583            // u8 + bool: 1 + 1 + 1
584            // MacroExpansionKind + bool: 1 + 1
585            Self::Calls { .. } | Self::MacroExpansion { .. } => 3,
586
587            // Option<StringId> + bool: 5 + 1 + 1 (imports/exports)
588            // DbQueryType + Option<StringId>: 1 + 5
589            Self::Imports { .. } | Self::Exports { .. } | Self::DbQuery { .. } => 7,
590
591            // FfiConvention: 1 byte
592            // LifetimeConstraintKind: 1 byte
593            Self::FfiCall { .. } | Self::LifetimeConstraint { .. } => 2,
594
595            // StringId + StringId + bool: 4 + 4 + 1
596            // StringId + Option<StringId>: 4 + 5
597            Self::TraitMethodBinding { .. }
598            | Self::TableRead { .. }
599            | Self::TriggeredBy { .. }
600            | Self::ProtocolCall { .. } => 10,
601
602            // HttpMethod + Option<StringId>: 1 + 5
603            // Option<StringId>: 5 (websocket/file IPC)
604            Self::HttpRequest { .. } | Self::WebSocket { .. } | Self::FileIpc { .. } => 6,
605
606            // Two StringIds: 4 + 4
607            Self::GrpcCall { .. } => 9,
608
609            // StringId + Option<StringId> + TableWriteOp: 4 + 5 + 1
610            // Option<TypeOfContext> + Option<u16> + Option<StringId>: 2 + 3 + 5
611            Self::TableWrite { .. } | Self::MessageQueue { .. } | Self::TypeOf { .. } => 11,
612
613            // StringId: 4
614            Self::GraphQLOperation { .. } | Self::ProcessExec { .. } => 5,
615        }
616    }
617}
618
619impl fmt::Display for EdgeKind {
620    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
621        f.write_str(self.tag())
622    }
623}
624
625impl Default for EdgeKind {
626    /// Returns `EdgeKind::Calls` as the default (most common edge type).
627    fn default() -> Self {
628        Self::Calls {
629            argument_count: 0,
630            is_async: false,
631        }
632    }
633}
634
635#[cfg(test)]
636mod tests {
637    use super::*;
638
639    /// Helper to create a default Calls variant for tests.
640    fn calls() -> EdgeKind {
641        EdgeKind::Calls {
642            argument_count: 0,
643            is_async: false,
644        }
645    }
646
647    /// Helper to create a default Imports variant for tests.
648    fn imports() -> EdgeKind {
649        EdgeKind::Imports {
650            alias: None,
651            is_wildcard: false,
652        }
653    }
654
655    /// Helper to create a default Exports variant for tests.
656    fn exports() -> EdgeKind {
657        EdgeKind::Exports {
658            kind: ExportKind::Direct,
659            alias: None,
660        }
661    }
662
663    #[test]
664    fn test_edge_kind_tag() {
665        assert_eq!(calls().tag(), "calls");
666        assert_eq!(imports().tag(), "imports");
667        assert_eq!(exports().tag(), "exports");
668        assert_eq!(EdgeKind::Defines.tag(), "defines");
669        assert_eq!(
670            EdgeKind::HttpRequest {
671                method: HttpMethod::Get,
672                url: None
673            }
674            .tag(),
675            "http_request"
676        );
677    }
678
679    #[test]
680    fn test_edge_kind_display() {
681        assert_eq!(format!("{}", calls()), "calls");
682        assert_eq!(format!("{}", imports()), "imports");
683        assert_eq!(format!("{}", exports()), "exports");
684        assert_eq!(format!("{}", EdgeKind::Inherits), "inherits");
685    }
686
687    #[test]
688    fn test_is_call() {
689        assert!(calls().is_call());
690        assert!(
691            EdgeKind::Calls {
692                argument_count: 5,
693                is_async: true
694            }
695            .is_call()
696        );
697        assert!(
698            EdgeKind::FfiCall {
699                convention: FfiConvention::C
700            }
701            .is_call()
702        );
703        assert!(
704            EdgeKind::HttpRequest {
705                method: HttpMethod::Post,
706                url: None
707            }
708            .is_call()
709        );
710        assert!(!EdgeKind::Defines.is_call());
711        assert!(!EdgeKind::Inherits.is_call());
712        assert!(!imports().is_call());
713        assert!(!exports().is_call());
714    }
715
716    #[test]
717    fn test_is_structural() {
718        assert!(EdgeKind::Defines.is_structural());
719        assert!(EdgeKind::Contains.is_structural());
720        assert!(!calls().is_structural());
721        assert!(!imports().is_structural());
722        assert!(!exports().is_structural());
723    }
724
725    #[test]
726    fn test_is_type_relation() {
727        assert!(EdgeKind::Inherits.is_type_relation());
728        assert!(EdgeKind::Implements.is_type_relation());
729        assert!(
730            EdgeKind::TypeOf {
731                context: None,
732                index: None,
733                name: None,
734            }
735            .is_type_relation()
736        );
737        assert!(!calls().is_type_relation());
738    }
739
740    #[test]
741    fn test_is_cross_boundary() {
742        assert!(
743            EdgeKind::FfiCall {
744                convention: FfiConvention::C
745            }
746            .is_cross_boundary()
747        );
748        assert!(
749            EdgeKind::HttpRequest {
750                method: HttpMethod::Get,
751                url: None
752            }
753            .is_cross_boundary()
754        );
755        assert!(
756            EdgeKind::GrpcCall {
757                service: StringId::INVALID,
758                method: StringId::INVALID
759            }
760            .is_cross_boundary()
761        );
762        assert!(!calls().is_cross_boundary());
763        assert!(!imports().is_cross_boundary());
764        assert!(!exports().is_cross_boundary());
765    }
766
767    #[test]
768    fn test_is_async() {
769        assert!(
770            EdgeKind::MessageQueue {
771                protocol: MqProtocol::Kafka,
772                topic: None
773            }
774            .is_async()
775        );
776        assert!(EdgeKind::WebSocket { event: None }.is_async());
777        assert!(!calls().is_async());
778        // Note: EdgeKind::Calls with is_async: true still returns false from is_async()
779        // because is_async() refers to async communication patterns, not async function calls
780        assert!(
781            !EdgeKind::Calls {
782                argument_count: 0,
783                is_async: true
784            }
785            .is_async()
786        );
787    }
788
789    #[test]
790    fn test_default() {
791        assert_eq!(EdgeKind::default(), calls());
792        assert_eq!(HttpMethod::default(), HttpMethod::Get);
793        assert_eq!(FfiConvention::default(), FfiConvention::C);
794        assert_eq!(DbQueryType::default(), DbQueryType::Select);
795        assert_eq!(ExportKind::default(), ExportKind::Direct);
796    }
797
798    #[test]
799    fn test_http_method_as_str() {
800        assert_eq!(HttpMethod::Get.as_str(), "GET");
801        assert_eq!(HttpMethod::Post.as_str(), "POST");
802        assert_eq!(HttpMethod::Delete.as_str(), "DELETE");
803        assert_eq!(HttpMethod::All.as_str(), "ALL");
804    }
805
806    #[test]
807    fn test_calls_with_metadata() {
808        let sync_call = EdgeKind::Calls {
809            argument_count: 3,
810            is_async: false,
811        };
812        let async_call = EdgeKind::Calls {
813            argument_count: 0,
814            is_async: true,
815        };
816        assert_eq!(sync_call.tag(), "calls");
817        assert_eq!(async_call.tag(), "calls");
818        assert!(sync_call.is_call());
819        assert!(async_call.is_call());
820        assert_ne!(sync_call, async_call);
821    }
822
823    #[test]
824    fn test_imports_with_metadata() {
825        let simple = imports();
826        let aliased = EdgeKind::Imports {
827            alias: Some(StringId::new(42)),
828            is_wildcard: false,
829        };
830        let wildcard = EdgeKind::Imports {
831            alias: None,
832            is_wildcard: true,
833        };
834
835        assert_eq!(simple.tag(), "imports");
836        assert_eq!(aliased.tag(), "imports");
837        assert_eq!(wildcard.tag(), "imports");
838        assert_ne!(simple, aliased);
839        assert_ne!(simple, wildcard);
840    }
841
842    #[test]
843    fn test_exports_with_metadata() {
844        let direct = exports();
845        let reexport = EdgeKind::Exports {
846            kind: ExportKind::Reexport,
847            alias: None,
848        };
849        let default_export = EdgeKind::Exports {
850            kind: ExportKind::Default,
851            alias: None,
852        };
853        let namespace = EdgeKind::Exports {
854            kind: ExportKind::Namespace,
855            alias: Some(StringId::new(1)),
856        };
857
858        assert_eq!(direct.tag(), "exports");
859        assert_eq!(reexport.tag(), "exports");
860        assert_eq!(default_export.tag(), "exports");
861        assert_eq!(namespace.tag(), "exports");
862        assert_ne!(direct, reexport);
863        assert_ne!(direct, default_export);
864    }
865
866    #[test]
867    fn test_serde_calls_imports_exports() {
868        // Calls with metadata
869        let calls = EdgeKind::Calls {
870            argument_count: 5,
871            is_async: true,
872        };
873        let json = serde_json::to_string(&calls).unwrap();
874        let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
875        assert_eq!(calls, deserialized);
876        assert!(json.contains("\"calls\""));
877        assert!(json.contains("\"argument_count\":5"));
878        assert!(json.contains("\"is_async\":true"));
879
880        // Imports with alias
881        let imports = EdgeKind::Imports {
882            alias: Some(StringId::new(10)),
883            is_wildcard: false,
884        };
885        let json = serde_json::to_string(&imports).unwrap();
886        let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
887        assert_eq!(imports, deserialized);
888
889        // Exports with kind
890        let exports = EdgeKind::Exports {
891            kind: ExportKind::Reexport,
892            alias: None,
893        };
894        let json = serde_json::to_string(&exports).unwrap();
895        let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
896        assert_eq!(exports, deserialized);
897    }
898
899    #[test]
900    fn test_serde_complex_variants() {
901        // HttpRequest with fields
902        let http = EdgeKind::HttpRequest {
903            method: HttpMethod::Post,
904            url: None,
905        };
906        let json = serde_json::to_string(&http).unwrap();
907        let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
908        assert_eq!(http, deserialized);
909
910        // GrpcCall with StringIds
911        let grpc = EdgeKind::GrpcCall {
912            service: StringId::new(1),
913            method: StringId::new(2),
914        };
915        let json = serde_json::to_string(&grpc).unwrap();
916        let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
917        assert_eq!(grpc, deserialized);
918    }
919
920    #[test]
921    fn test_postcard_roundtrip_simple_enums() {
922        // Test postcard roundtrip for component enums used by EdgeKind.
923
924        // FfiConvention
925        for conv in [
926            FfiConvention::C,
927            FfiConvention::Cdecl,
928            FfiConvention::Stdcall,
929        ] {
930            let bytes = postcard::to_allocvec(&conv).unwrap();
931            let deserialized: FfiConvention = postcard::from_bytes(&bytes).unwrap();
932            assert_eq!(conv, deserialized);
933        }
934
935        // HttpMethod
936        for method in [
937            HttpMethod::Get,
938            HttpMethod::Post,
939            HttpMethod::Delete,
940            HttpMethod::All,
941        ] {
942            let bytes = postcard::to_allocvec(&method).unwrap();
943            let deserialized: HttpMethod = postcard::from_bytes(&bytes).unwrap();
944            assert_eq!(method, deserialized);
945        }
946
947        // DbQueryType
948        for query in [
949            DbQueryType::Select,
950            DbQueryType::Insert,
951            DbQueryType::Update,
952        ] {
953            let bytes = postcard::to_allocvec(&query).unwrap();
954            let deserialized: DbQueryType = postcard::from_bytes(&bytes).unwrap();
955            assert_eq!(query, deserialized);
956        }
957
958        // ExportKind
959        for kind in [
960            ExportKind::Direct,
961            ExportKind::Reexport,
962            ExportKind::Default,
963            ExportKind::Namespace,
964        ] {
965            let bytes = postcard::to_allocvec(&kind).unwrap();
966            let deserialized: ExportKind = postcard::from_bytes(&bytes).unwrap();
967            assert_eq!(kind, deserialized);
968        }
969    }
970
971    #[test]
972    fn test_edge_kind_json_compatibility() {
973        // EdgeKind is designed for JSON serialization (MCP export).
974        // Binary persistence in Phase 6 will use a custom format.
975        let kinds = [
976            calls(),
977            imports(),
978            exports(),
979            EdgeKind::Defines,
980            EdgeKind::HttpRequest {
981                method: HttpMethod::Get,
982                url: None,
983            },
984            EdgeKind::MessageQueue {
985                protocol: MqProtocol::Kafka,
986                topic: Some(StringId::new(1)),
987            },
988        ];
989
990        for kind in &kinds {
991            // JSON roundtrip should work
992            let json = serde_json::to_string(kind).unwrap();
993            let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
994            assert_eq!(*kind, deserialized);
995
996            // Postcard roundtrip should also work (required for graph persistence)
997            let bytes = postcard::to_allocvec(kind).unwrap();
998            let from_postcard: EdgeKind = postcard::from_bytes(&bytes).unwrap();
999            assert_eq!(*kind, from_postcard);
1000        }
1001    }
1002
1003    #[test]
1004    fn test_hash() {
1005        use std::collections::HashSet;
1006
1007        let mut set = HashSet::new();
1008        set.insert(calls());
1009        set.insert(imports());
1010        set.insert(exports());
1011        set.insert(EdgeKind::Defines);
1012        set.insert(EdgeKind::HttpRequest {
1013            method: HttpMethod::Get,
1014            url: None,
1015        });
1016
1017        assert!(set.contains(&calls()));
1018        assert!(set.contains(&imports()));
1019        assert!(set.contains(&exports()));
1020        assert!(!set.contains(&EdgeKind::Inherits));
1021        assert_eq!(set.len(), 5);
1022    }
1023
1024    #[test]
1025    fn test_ffi_convention_variants() {
1026        let conventions = [
1027            FfiConvention::C,
1028            FfiConvention::Cdecl,
1029            FfiConvention::Stdcall,
1030            FfiConvention::Fastcall,
1031            FfiConvention::System,
1032        ];
1033
1034        for conv in conventions {
1035            let edge = EdgeKind::FfiCall { convention: conv };
1036            assert!(edge.is_call());
1037            assert!(edge.is_cross_boundary());
1038        }
1039    }
1040
1041    #[test]
1042    fn test_mq_protocol_variants() {
1043        let protocols = [
1044            MqProtocol::Kafka,
1045            MqProtocol::Sqs,
1046            MqProtocol::RabbitMq,
1047            MqProtocol::Nats,
1048            MqProtocol::Redis,
1049            MqProtocol::Other(StringId::new(1)),
1050        ];
1051
1052        for proto in protocols {
1053            let edge = EdgeKind::MessageQueue {
1054                protocol: proto.clone(),
1055                topic: None,
1056            };
1057            assert!(edge.is_async());
1058            assert!(edge.is_cross_boundary());
1059        }
1060    }
1061
1062    #[test]
1063    fn test_export_kind_variants() {
1064        let kinds = [
1065            ExportKind::Direct,
1066            ExportKind::Reexport,
1067            ExportKind::Default,
1068            ExportKind::Namespace,
1069        ];
1070
1071        for kind in kinds {
1072            let edge = EdgeKind::Exports { kind, alias: None };
1073            assert_eq!(edge.tag(), "exports");
1074            assert!(!edge.is_call());
1075            assert!(!edge.is_structural());
1076            assert!(!edge.is_cross_boundary());
1077        }
1078    }
1079
1080    #[test]
1081    fn test_estimated_size() {
1082        // Unit variants
1083        assert_eq!(EdgeKind::Defines.estimated_size(), 1);
1084        assert_eq!(EdgeKind::Contains.estimated_size(), 1);
1085        assert_eq!(EdgeKind::References.estimated_size(), 1);
1086
1087        // Calls: u8 + bool = 3
1088        assert_eq!(calls().estimated_size(), 3);
1089
1090        // Imports: Option<StringId> + bool = 7
1091        assert_eq!(imports().estimated_size(), 7);
1092
1093        // Exports: ExportKind + Option<StringId> = 7
1094        assert_eq!(exports().estimated_size(), 7);
1095
1096        // Rust-specific edges
1097        assert_eq!(
1098            EdgeKind::LifetimeConstraint {
1099                constraint_kind: LifetimeConstraintKind::Outlives
1100            }
1101            .estimated_size(),
1102            2
1103        );
1104        assert_eq!(
1105            EdgeKind::MacroExpansion {
1106                expansion_kind: MacroExpansionKind::Derive,
1107                is_verified: true
1108            }
1109            .estimated_size(),
1110            3
1111        );
1112        assert_eq!(
1113            EdgeKind::TraitMethodBinding {
1114                trait_name: StringId::INVALID,
1115                impl_type: StringId::INVALID,
1116                is_ambiguous: false
1117            }
1118            .estimated_size(),
1119            10
1120        );
1121    }
1122
1123    // ==================== Rust-Specific Edge Tests ====================
1124
1125    #[test]
1126    fn test_lifetime_constraint_kind_variants() {
1127        let kinds = [
1128            LifetimeConstraintKind::Outlives,
1129            LifetimeConstraintKind::TypeBound,
1130            LifetimeConstraintKind::Reference,
1131            LifetimeConstraintKind::Static,
1132            LifetimeConstraintKind::HigherRanked,
1133            LifetimeConstraintKind::TraitObject,
1134            LifetimeConstraintKind::ImplTrait,
1135            LifetimeConstraintKind::Elided,
1136        ];
1137
1138        for constraint_kind in kinds {
1139            let edge = EdgeKind::LifetimeConstraint { constraint_kind };
1140            assert!(edge.is_rust_specific());
1141            assert!(edge.is_lifetime_constraint());
1142            assert!(!edge.is_call());
1143            assert!(!edge.is_structural());
1144            assert_eq!(edge.tag(), "lifetime_constraint");
1145        }
1146    }
1147
1148    #[test]
1149    fn test_macro_expansion_kind_variants() {
1150        let kinds = [
1151            MacroExpansionKind::Derive,
1152            MacroExpansionKind::Attribute,
1153            MacroExpansionKind::Declarative,
1154            MacroExpansionKind::Function,
1155            MacroExpansionKind::CfgGate,
1156        ];
1157
1158        for expansion_kind in kinds {
1159            let edge = EdgeKind::MacroExpansion {
1160                expansion_kind,
1161                is_verified: true,
1162            };
1163            assert!(edge.is_rust_specific());
1164            assert!(edge.is_macro_expansion());
1165            assert!(!edge.is_call());
1166            assert_eq!(edge.tag(), "macro_expansion");
1167        }
1168    }
1169
1170    #[test]
1171    fn test_trait_method_binding() {
1172        let edge = EdgeKind::TraitMethodBinding {
1173            trait_name: StringId::new(1),
1174            impl_type: StringId::new(2),
1175            is_ambiguous: false,
1176        };
1177
1178        assert!(edge.is_rust_specific());
1179        assert!(edge.is_trait_method_binding());
1180        assert!(!edge.is_call());
1181        assert_eq!(edge.tag(), "trait_method_binding");
1182
1183        // Test ambiguous binding
1184        let ambiguous = EdgeKind::TraitMethodBinding {
1185            trait_name: StringId::new(1),
1186            impl_type: StringId::new(2),
1187            is_ambiguous: true,
1188        };
1189        assert!(ambiguous.is_trait_method_binding());
1190    }
1191
1192    #[test]
1193    fn test_rust_specific_edges_serde() {
1194        // LifetimeConstraint
1195        let lifetime = EdgeKind::LifetimeConstraint {
1196            constraint_kind: LifetimeConstraintKind::HigherRanked,
1197        };
1198        let json = serde_json::to_string(&lifetime).unwrap();
1199        let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
1200        assert_eq!(lifetime, deserialized);
1201
1202        // TraitMethodBinding
1203        let binding = EdgeKind::TraitMethodBinding {
1204            trait_name: StringId::new(10),
1205            impl_type: StringId::new(20),
1206            is_ambiguous: true,
1207        };
1208        let json = serde_json::to_string(&binding).unwrap();
1209        let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
1210        assert_eq!(binding, deserialized);
1211
1212        // MacroExpansion
1213        let expansion = EdgeKind::MacroExpansion {
1214            expansion_kind: MacroExpansionKind::Derive,
1215            is_verified: false,
1216        };
1217        let json = serde_json::to_string(&expansion).unwrap();
1218        let deserialized: EdgeKind = serde_json::from_str(&json).unwrap();
1219        assert_eq!(expansion, deserialized);
1220    }
1221
1222    #[test]
1223    fn test_rust_specific_edges_postcard() {
1224        let edges = [
1225            EdgeKind::LifetimeConstraint {
1226                constraint_kind: LifetimeConstraintKind::Outlives,
1227            },
1228            EdgeKind::TraitMethodBinding {
1229                trait_name: StringId::new(5),
1230                impl_type: StringId::new(6),
1231                is_ambiguous: false,
1232            },
1233            EdgeKind::MacroExpansion {
1234                expansion_kind: MacroExpansionKind::Attribute,
1235                is_verified: true,
1236            },
1237        ];
1238
1239        for edge in edges {
1240            let bytes = postcard::to_allocvec(&edge).unwrap();
1241            let deserialized: EdgeKind = postcard::from_bytes(&bytes).unwrap();
1242            assert_eq!(edge, deserialized);
1243        }
1244    }
1245
1246    #[test]
1247    fn test_lifetime_constraint_kind_defaults() {
1248        assert_eq!(
1249            LifetimeConstraintKind::default(),
1250            LifetimeConstraintKind::Outlives
1251        );
1252        assert_eq!(
1253            MacroExpansionKind::default(),
1254            MacroExpansionKind::Declarative
1255        );
1256    }
1257}