rubydex 0.3.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +29 -7
  3. data/THIRD_PARTY_LICENSES.html +238 -2
  4. data/exe/rdx +2 -149
  5. data/ext/rubydex/config.c +140 -0
  6. data/ext/rubydex/config.h +16 -0
  7. data/ext/rubydex/definition.c +34 -0
  8. data/ext/rubydex/definition.h +3 -0
  9. data/ext/rubydex/diagnostic.c +28 -1
  10. data/ext/rubydex/diagnostic.h +2 -0
  11. data/ext/rubydex/extconf.rb +4 -2
  12. data/ext/rubydex/graph.c +71 -55
  13. data/ext/rubydex/graph.h +6 -0
  14. data/ext/rubydex/query.c +398 -16
  15. data/ext/rubydex/rubydex.c +2 -0
  16. data/ext/rubydex/utils.c +11 -4
  17. data/lib/ruby_lsp/rubydex/addon.rb +211 -0
  18. data/lib/rubydex/cli/command/console.rb +55 -0
  19. data/lib/rubydex/cli/command/lint/explain.rb +76 -0
  20. data/lib/rubydex/cli/command/lint.rb +208 -0
  21. data/lib/rubydex/cli/command/mcp.rb +30 -0
  22. data/lib/rubydex/cli/command/query.rb +70 -0
  23. data/lib/rubydex/cli/command/skill.rb +69 -0
  24. data/lib/rubydex/cli/command.rb +168 -0
  25. data/lib/rubydex/cli.rb +93 -0
  26. data/lib/rubydex/config.rb +59 -0
  27. data/lib/rubydex/diagnostic.rb +12 -3
  28. data/lib/rubydex/errors.rb +42 -1
  29. data/lib/rubydex/graph.rb +10 -3
  30. data/lib/rubydex/linter/custom_rule.rb +97 -0
  31. data/lib/rubydex/linter/helpers/path_helpers.rb +78 -0
  32. data/lib/rubydex/linter/helpers/source_access_helpers.rb +31 -0
  33. data/lib/rubydex/linter/rule_loader.rb +36 -0
  34. data/lib/rubydex/linter/rule_test_case.rb +343 -0
  35. data/lib/rubydex/linter/runner.rb +56 -0
  36. data/lib/rubydex/linter.rb +19 -0
  37. data/lib/rubydex/location.rb +3 -0
  38. data/lib/rubydex/mcp_server.rb +1 -2
  39. data/lib/rubydex/related_information.rb +17 -0
  40. data/lib/rubydex/rule.rb +33 -0
  41. data/lib/rubydex/rules/dynamic_ancestor.rb +29 -0
  42. data/lib/rubydex/rules/dynamic_constant_reference.rb +25 -0
  43. data/lib/rubydex/rules/dynamic_singleton_definition.rb +30 -0
  44. data/lib/rubydex/rules/invalid_constant_visibility.rb +28 -0
  45. data/lib/rubydex/rules/invalid_method_visibility.rb +28 -0
  46. data/lib/rubydex/rules/parse_error.rb +25 -0
  47. data/lib/rubydex/rules/parse_warning.rb +28 -0
  48. data/lib/rubydex/rules/top_level_mixin_self.rb +27 -0
  49. data/lib/rubydex/rules/undefined_constant_visibility_target.rb +27 -0
  50. data/lib/rubydex/rules/undefined_method_visibility_target.rb +27 -0
  51. data/lib/rubydex/severity.rb +70 -0
  52. data/lib/rubydex/skill.rb +89 -0
  53. data/lib/rubydex/skill_registry.rb +62 -0
  54. data/lib/rubydex/version.rb +1 -1
  55. data/lib/rubydex.rb +8 -0
  56. data/lib/rubydex_linter/rules/rule_structure.rb +140 -0
  57. data/rbi/rubydex.rbi +464 -21
  58. data/rust/Cargo.lock +2 -2
  59. data/rust/rubydex/Cargo.toml +5 -1
  60. data/rust/rubydex/benches/graph_memory.rs +3 -5
  61. data/rust/rubydex/src/bin/generate_ruby_rules.rs +94 -0
  62. data/rust/rubydex/src/config.rs +538 -157
  63. data/rust/rubydex/src/diagnostic.rs +162 -45
  64. data/rust/rubydex/src/errors.rs +0 -1
  65. data/rust/rubydex/src/indexing/local_graph.rs +6 -5
  66. data/rust/rubydex/src/indexing/rbs_indexer.rs +270 -6
  67. data/rust/rubydex/src/indexing/ruby_indexer.rs +10 -12
  68. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +134 -81
  69. data/rust/rubydex/src/lib.rs +1 -0
  70. data/rust/rubydex/src/listing.rs +26 -1
  71. data/rust/rubydex/src/main.rs +7 -4
  72. data/rust/rubydex/src/model/declaration.rs +302 -219
  73. data/rust/rubydex/src/model/graph.rs +27 -40
  74. data/rust/rubydex/src/model/name.rs +58 -17
  75. data/rust/rubydex/src/operation/ruby_builder.rs +30 -45
  76. data/rust/rubydex/src/path_helpers.rs +77 -0
  77. data/rust/rubydex/src/query/cypher/schema.rs +63 -0
  78. data/rust/rubydex/src/query/cypher/tests.rs +26 -1
  79. data/rust/rubydex/src/query/cypher.rs +8 -11
  80. data/rust/rubydex/src/query.rs +314 -44
  81. data/rust/rubydex/src/resolution.rs +139 -187
  82. data/rust/rubydex/src/resolution_tests.rs +247 -19
  83. data/rust/rubydex/src/test_utils/context.rs +2 -1
  84. data/rust/rubydex/src/test_utils/graph_test.rs +26 -12
  85. data/rust/rubydex/src/test_utils/local_graph_test.rs +19 -0
  86. data/rust/rubydex/tests/cli.rs +4 -4
  87. data/rust/rubydex-sys/src/config_api.rs +205 -0
  88. data/rust/rubydex-sys/src/cypher_api.rs +791 -0
  89. data/rust/rubydex-sys/src/definition_api.rs +68 -1
  90. data/rust/rubydex-sys/src/diagnostic_api.rs +42 -8
  91. data/rust/rubydex-sys/src/graph_api.rs +125 -205
  92. data/rust/rubydex-sys/src/lib.rs +2 -0
  93. data/rust/rubydex-sys/src/name_api.rs +49 -17
  94. data/rust/rubydex-sys/src/utils.rs +37 -0
  95. data/skills/send-private-method/SKILL.md +133 -0
  96. metadata +42 -2
@@ -6,7 +6,9 @@ use crate::location_api::{Location, create_location_for_uri_and_offset};
6
6
  use crate::reference_api::CConstantReference;
7
7
  use libc::c_char;
8
8
  use rubydex::model::definitions::{Definition, Mixin};
9
- use rubydex::model::ids::DefinitionId;
9
+ use rubydex::model::graph::Graph;
10
+ use rubydex::model::ids::{DefinitionId, NameId};
11
+ use rubydex::model::name::ParentScope;
10
12
  use rubydex::query::AliasResolutionError;
11
13
  use std::ffi::CString;
12
14
  use std::ptr;
@@ -110,6 +112,71 @@ pub unsafe extern "C" fn rdx_definition_name(pointer: GraphPointer, definition_i
110
112
  })
111
113
  }
112
114
 
115
+ fn raw_name_for_name(graph: &Graph, name_id: NameId) -> String {
116
+ let mut raw_name = String::new();
117
+ append_raw_name(graph, name_id, &mut raw_name);
118
+ raw_name
119
+ }
120
+
121
+ fn append_raw_name(graph: &Graph, name_id: NameId, raw_name: &mut String) {
122
+ let name = graph
123
+ .names()
124
+ .get(&name_id)
125
+ .expect("name should exist while building raw_name");
126
+ let simple_name = graph
127
+ .strings()
128
+ .get(name.str())
129
+ .expect("string should exist while building raw_name")
130
+ .as_str();
131
+
132
+ match name.parent_scope() {
133
+ ParentScope::None => {}
134
+ ParentScope::TopLevel => raw_name.push_str("::"),
135
+ ParentScope::Some(parent_id) | ParentScope::Attached(parent_id) => {
136
+ append_raw_name(graph, *parent_id, raw_name);
137
+ raw_name.push_str("::");
138
+ }
139
+ }
140
+
141
+ raw_name.push_str(simple_name);
142
+ }
143
+
144
+ fn raw_name_id(definition: &Definition) -> Option<&NameId> {
145
+ match definition {
146
+ Definition::Class(_) | Definition::Module(_) | Definition::Constant(_) | Definition::ConstantAlias(_) => {
147
+ definition.name_id()
148
+ }
149
+ _ => None,
150
+ }
151
+ }
152
+
153
+ /// Returns the UTF-8 raw name string for a class, module, constant, or constant-alias definition id.
154
+ ///
155
+ /// The raw name is reconstructed from the definition's `Name`, preserving explicit parent scopes like `Foo::Bar` and
156
+ /// rooted paths like `::Bar`. Returns null if the definition cannot be found or does not support raw names. Caller must
157
+ /// free a non-null result with `free_c_string`.
158
+ ///
159
+ /// # Panics
160
+ ///
161
+ /// Panics if the graph contains an invalid name or string reference, or if the name contains an embedded null byte.
162
+ ///
163
+ /// # Safety
164
+ ///
165
+ /// Assumes pointer is valid.
166
+ ///
167
+ #[unsafe(no_mangle)]
168
+ pub unsafe extern "C" fn rdx_definition_raw_name(pointer: GraphPointer, definition_id: u64) -> *const c_char {
169
+ with_graph(pointer, |graph| {
170
+ let def_id = DefinitionId::new(definition_id);
171
+ let Some(name_id) = graph.definitions().get(&def_id).and_then(raw_name_id) else {
172
+ return ptr::null();
173
+ };
174
+
175
+ let name = raw_name_for_name(graph, *name_id);
176
+ CString::new(name).unwrap().into_raw().cast_const()
177
+ })
178
+ }
179
+
113
180
  /// Shared iterator over definition (id, kind) pairs
114
181
  #[derive(Debug)]
115
182
  pub struct DefinitionsIter {
@@ -3,12 +3,52 @@
3
3
  use crate::graph_api::{GraphPointer, with_graph};
4
4
  use crate::location_api::{Location, create_location_for_uri_and_offset};
5
5
  use libc::c_char;
6
+ use rubydex::diagnostic::{Rule, Severity};
6
7
  use std::{ffi::CString, mem, ptr};
7
8
 
9
+ /// C-compatible enum representing diagnostic severity levels.
10
+ #[repr(C)]
11
+ #[derive(Debug, Copy, Clone, PartialEq, Eq)]
12
+ pub enum DiagnosticSeverity {
13
+ Error = 1,
14
+ Warning = 2,
15
+ Information = 3,
16
+ Hint = 4,
17
+ }
18
+
19
+ impl From<Severity> for DiagnosticSeverity {
20
+ fn from(severity: Severity) -> Self {
21
+ match severity {
22
+ Severity::Error => DiagnosticSeverity::Error,
23
+ Severity::Warning => DiagnosticSeverity::Warning,
24
+ Severity::Information => DiagnosticSeverity::Information,
25
+ Severity::Hint => DiagnosticSeverity::Hint,
26
+ }
27
+ }
28
+ }
29
+
30
+ #[repr(C)]
31
+ #[derive(Debug)]
32
+ pub struct CRule {
33
+ pub name: *const c_char,
34
+ pub name_length: usize,
35
+ }
36
+
37
+ impl From<Rule> for CRule {
38
+ fn from(rule: Rule) -> Self {
39
+ let name = rule.name();
40
+
41
+ Self {
42
+ name: name.as_ptr().cast::<c_char>(),
43
+ name_length: name.len(),
44
+ }
45
+ }
46
+ }
47
+
8
48
  /// C-compatible struct representing a diagnostic entry.
9
49
  #[repr(C)]
10
50
  pub struct DiagnosticEntry {
11
- pub rule: *const c_char,
51
+ pub rule: CRule,
12
52
  pub message: *const c_char,
13
53
  pub location: *mut Location,
14
54
  }
@@ -50,10 +90,7 @@ pub unsafe extern "C" fn rdx_graph_diagnostics(pointer: GraphPointer) -> *mut Di
50
90
  let location = create_location_for_uri_and_offset(graph, document, diagnostic.offset());
51
91
 
52
92
  DiagnosticEntry {
53
- rule: CString::new(diagnostic.rule().to_string())
54
- .unwrap()
55
- .into_raw()
56
- .cast_const(),
93
+ rule: CRule::from(*diagnostic.rule()),
57
94
  message: CString::new(diagnostic.message()).unwrap().into_raw().cast_const(),
58
95
  location,
59
96
  }
@@ -82,9 +119,6 @@ pub unsafe extern "C" fn rdx_diagnostics_free(ptr: *mut DiagnosticArray) {
82
119
  let mut boxed_slice: Box<[DiagnosticEntry]> = unsafe { Box::from_raw(slice_ptr) };
83
120
 
84
121
  for entry in &mut *boxed_slice {
85
- if !entry.rule.is_null() {
86
- let _ = unsafe { CString::from_raw(entry.rule.cast_mut()) };
87
- }
88
122
  if !entry.message.is_null() {
89
123
  let _ = unsafe { CString::from_raw(entry.message.cast_mut()) };
90
124
  }
@@ -1,5 +1,6 @@
1
1
  //! This file provides the C API for the Graph object
2
2
 
3
+ use crate::config_api::ConfigPointer;
3
4
  use crate::declaration_api::CDeclaration;
4
5
  use crate::declaration_api::DeclarationsIter;
5
6
  use crate::declaration_api::decl_id_from_char_ptr;
@@ -7,20 +8,20 @@ use crate::document_api::DocumentsIter;
7
8
  use crate::reference_api::{CConstantReference, CMethodReference, ConstantReferencesIter, MethodReferencesIter};
8
9
  use crate::{name_api, utils};
9
10
  use libc::{c_char, c_void};
10
- use rubydex::errors::Errors;
11
+ use rubydex::config::Config;
11
12
  use rubydex::indexing::LanguageId;
13
+ use rubydex::model::definitions::Definition;
12
14
  use rubydex::model::encoding::Encoding;
13
15
  use rubydex::model::graph::Graph;
14
- use rubydex::model::ids::{DeclarationId, NameId, UriId, declaration_id_from_lookup_name};
16
+ use rubydex::model::ids::{DeclarationId, DefinitionId, NameId, UriId, declaration_id_from_lookup_name};
15
17
  use rubydex::model::keywords;
16
18
  use rubydex::model::name::NameRef;
17
19
  use rubydex::model::visibility::Visibility;
18
- use rubydex::query::cypher::{self, OutputFormat};
19
20
  use rubydex::query::{CompletionCandidate, CompletionContext, CompletionReceiver};
20
21
  use rubydex::resolution::Resolver;
21
22
  use rubydex::{indexing, integrity, listing, query};
22
23
  use std::ffi::CString;
23
- use std::path::{Path, PathBuf};
24
+ use std::path::PathBuf;
24
25
  use std::{mem, ptr, sync::RwLock};
25
26
 
26
27
  pub type GraphPointer = *mut c_void;
@@ -133,6 +134,27 @@ pub unsafe extern "C" fn rdx_graph_declarations_fuzzy_search(
133
134
  DeclarationsIter::new(entries)
134
135
  }
135
136
 
137
+ /// Returns an iterator over all dead code candidates in the graph.
138
+ ///
139
+ /// # Safety
140
+ ///
141
+ /// Expects `pointer` to be a valid graph. The returned iterator must be freed with `rdx_graph_declarations_iter_free`.
142
+ #[unsafe(no_mangle)]
143
+ pub unsafe extern "C" fn rdx_graph_dead_code_candidates(pointer: GraphPointer) -> *mut DeclarationsIter {
144
+ let entries = with_graph(pointer, |graph| {
145
+ query::dead_code_candidates(graph)
146
+ .into_iter()
147
+ .filter_map(|id| {
148
+ let decl = graph.declarations().get(&id)?;
149
+ Some(CDeclaration::from_declaration(id, decl))
150
+ })
151
+ .collect::<Vec<CDeclaration>>()
152
+ .into_boxed_slice()
153
+ });
154
+
155
+ DeclarationsIter::new(entries)
156
+ }
157
+
136
158
  /// # Panics
137
159
  ///
138
160
  /// Will panic if the nesting cannot be transformed into a vector of strings
@@ -155,24 +177,103 @@ pub unsafe extern "C" fn rdx_graph_resolve_constant(
155
177
  return ptr::null();
156
178
  };
157
179
 
158
- let mut resolver = Resolver::new(graph);
180
+ resolve_constant_name(graph, name_id, names_to_untrack)
181
+ })
182
+ }
159
183
 
160
- let declaration = match resolver.resolve_constant(name_id) {
161
- Some(id) => {
162
- let decl = graph.declarations().get(&id).unwrap();
163
- Box::into_raw(Box::new(CDeclaration::from_declaration(id, decl))).cast_const()
184
+ /// Result of resolving a constant using a namespace definition as its lexical context.
185
+ #[repr(u8)]
186
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
187
+ pub enum CDefinitionConstantResolution {
188
+ Resolved = 0,
189
+ NotFound = 1,
190
+ DefinitionNotFound = 2,
191
+ InvalidDefinitionKind = 3,
192
+ }
193
+
194
+ #[repr(C)]
195
+ #[derive(Debug)]
196
+ pub struct CDefinitionConstantResolutionResult {
197
+ pub status: CDefinitionConstantResolution,
198
+ pub declaration: *const CDeclaration,
199
+ }
200
+
201
+ /// Resolves a constant using a namespace definition as its lexical context.
202
+ ///
203
+ /// # Panics
204
+ ///
205
+ /// Panics if `const_name` cannot be converted to a string.
206
+ ///
207
+ /// # Safety
208
+ ///
209
+ /// Assumes that `pointer` and `const_name` are valid.
210
+ #[unsafe(no_mangle)]
211
+ pub unsafe extern "C" fn rdx_graph_resolve_constant_from_definition(
212
+ pointer: GraphPointer,
213
+ const_name: *const c_char,
214
+ definition_id: u64,
215
+ ) -> CDefinitionConstantResolutionResult {
216
+ with_mut_graph(pointer, |graph| {
217
+ let const_name: String = unsafe { utils::convert_char_ptr_to_string(const_name).unwrap() };
218
+ let nesting = match class_or_module_definition_name_id(graph, DefinitionId::new(definition_id)) {
219
+ Ok(nesting) => nesting,
220
+ Err(status) => {
221
+ return CDefinitionConstantResolutionResult {
222
+ status,
223
+ declaration: ptr::null(),
224
+ };
164
225
  }
165
- None => ptr::null(),
166
226
  };
167
227
 
168
- for name_id in names_to_untrack {
169
- graph.untrack_name(name_id);
170
- }
228
+ let Some((name_id, names_to_untrack)) = name_api::name_in_nesting_to_name_id(graph, &const_name, Some(nesting))
229
+ else {
230
+ return CDefinitionConstantResolutionResult {
231
+ status: CDefinitionConstantResolution::NotFound,
232
+ declaration: ptr::null(),
233
+ };
234
+ };
235
+
236
+ let declaration = resolve_constant_name(graph, name_id, names_to_untrack);
237
+ let status = if declaration.is_null() {
238
+ CDefinitionConstantResolution::NotFound
239
+ } else {
240
+ CDefinitionConstantResolution::Resolved
241
+ };
171
242
 
172
- declaration
243
+ CDefinitionConstantResolutionResult { status, declaration }
173
244
  })
174
245
  }
175
246
 
247
+ fn class_or_module_definition_name_id(
248
+ graph: &Graph,
249
+ definition_id: DefinitionId,
250
+ ) -> Result<NameId, CDefinitionConstantResolution> {
251
+ let Some(definition) = graph.definitions().get(&definition_id) else {
252
+ return Err(CDefinitionConstantResolution::DefinitionNotFound);
253
+ };
254
+
255
+ match definition {
256
+ Definition::Class(definition) => Ok(*definition.name_id()),
257
+ Definition::SingletonClass(definition) => Ok(*definition.name_id()),
258
+ Definition::Module(definition) => Ok(*definition.name_id()),
259
+ _ => Err(CDefinitionConstantResolution::InvalidDefinitionKind),
260
+ }
261
+ }
262
+
263
+ fn resolve_constant_name(graph: &mut Graph, name_id: NameId, names_to_untrack: Vec<NameId>) -> *const CDeclaration {
264
+ let declaration_id = Resolver::new(graph).resolve_constant(name_id);
265
+ let declaration = declaration_id.map_or(ptr::null(), |id| {
266
+ let declaration = graph.declarations().get(&id).unwrap();
267
+ Box::into_raw(Box::new(CDeclaration::from_declaration(id, declaration))).cast_const()
268
+ });
269
+
270
+ for name_id in names_to_untrack {
271
+ graph.untrack_name(name_id);
272
+ }
273
+
274
+ declaration
275
+ }
276
+
176
277
  /// Adds glob patterns to exclude from file discovery during indexing.
177
278
  ///
178
279
  /// # Panics
@@ -216,14 +317,8 @@ pub unsafe extern "C" fn rdx_graph_excluded_patterns(
216
317
 
217
318
  let c_strings: Vec<*const c_char> = excluded
218
319
  .iter()
219
- .filter_map(|path| {
220
- // Normalize all paths to use forward slashes. Otherwise, you get mixed backslashes and forward slashes
221
- // on Windows if a configuration file is using forward slashes. For example:
222
- //
223
- // C:\project/vendor/bundle
224
- let normalized = path.replace(std::path::MAIN_SEPARATOR, "/");
225
-
226
- CString::new(normalized)
320
+ .filter_map(|pattern| {
321
+ CString::new(pattern.as_ref())
227
322
  .ok()
228
323
  .map(|c_string| c_string.into_raw().cast_const())
229
324
  })
@@ -236,23 +331,6 @@ pub unsafe extern "C" fn rdx_graph_excluded_patterns(
236
331
  })
237
332
  }
238
333
 
239
- /// Sets the workspace path used as the root directory for indexing and relative path resolution. Silently ignores the
240
- /// call if the given path is not valid UTF-8, leaving the existing workspace path untouched (mirrors
241
- /// `rdx_graph_set_encoding`). This avoids unwinding across the FFI boundary on malformed input.
242
- ///
243
- /// # Safety
244
- ///
245
- /// - `pointer` must be a valid `GraphPointer` previously returned by this crate.
246
- /// - `path` must be a valid, null-terminated string.
247
- #[unsafe(no_mangle)]
248
- pub unsafe extern "C" fn rdx_graph_set_workspace_path(pointer: GraphPointer, path: *const c_char) {
249
- let Ok(path) = (unsafe { utils::convert_char_ptr_to_string(path) }) else {
250
- return;
251
- };
252
-
253
- with_mut_graph(pointer, |graph| graph.set_workspace_path(PathBuf::from(path)));
254
- }
255
-
256
334
  /// Returns the workspace path as a C string. Caller must free with `free_c_string`.
257
335
  ///
258
336
  /// # Safety
@@ -261,42 +339,23 @@ pub unsafe extern "C" fn rdx_graph_set_workspace_path(pointer: GraphPointer, pat
261
339
  #[unsafe(no_mangle)]
262
340
  pub unsafe extern "C" fn rdx_graph_workspace_path(pointer: GraphPointer) -> *const c_char {
263
341
  with_graph(pointer, |graph| {
264
- CString::new(graph.workspace_path().to_string_lossy().as_ref())
342
+ CString::new(utils::interop_path(graph.workspace_path()))
265
343
  .map_or(ptr::null(), |c_string| c_string.into_raw().cast_const())
266
344
  })
267
345
  }
268
346
 
269
- /// Loads configuration into the graph. A null `config_path` attempts to load the default configuration file.
270
- ///
271
- /// Returns NULL on success. On failure returns an owned, null-terminated error message that the caller must free with
272
- /// `free_c_string`.
273
- ///
274
- /// A `config_path` that is not valid UTF-8 is reported as an error message.
347
+ /// Applies a parsed configuration file to the graph, which adopts the workspace it was loaded for along with the
348
+ /// settings of its `[graph]` section. This is the only way to point the graph at a workspace other than the current
349
+ /// directory, and it replaces any previously applied configuration. Tool-specific sections are ignored.
275
350
  ///
276
351
  /// # Safety
277
352
  ///
278
353
  /// - `pointer` must be a valid `GraphPointer` previously returned by this crate.
279
- /// - `config_path` must either be NULL or a valid, null-terminated string.
354
+ /// - `config` must be a valid `ConfigPointer` previously returned by `rdx_config_load`.
280
355
  #[unsafe(no_mangle)]
281
- pub unsafe extern "C" fn rdx_graph_load_config(pointer: GraphPointer, config_path: *const c_char) -> *const c_char {
282
- let result = with_mut_graph(pointer, |graph| {
283
- if config_path.is_null() {
284
- graph.load_config(None)
285
- } else {
286
- match unsafe { utils::convert_char_ptr_to_string(config_path) } {
287
- Ok(config_path) => graph.load_config(Some(Path::new(&config_path))),
288
- Err(_) => Err(Errors::ConfigError("config file path is not valid UTF-8".to_string())),
289
- }
290
- }
291
- });
292
-
293
- match result {
294
- Ok(()) => ptr::null(),
295
- Err(error) => CString::new(error.to_string())
296
- .unwrap_or_default()
297
- .into_raw()
298
- .cast_const(),
299
- }
356
+ pub unsafe extern "C" fn rdx_graph_load_config(pointer: GraphPointer, config: ConfigPointer) {
357
+ let config = unsafe { &*config.cast::<Config>() };
358
+ with_mut_graph(pointer, |graph| graph.load_config(config));
300
359
  }
301
360
 
302
361
  /// Indexes all given file paths in parallel using the provided Graph pointer.
@@ -1066,145 +1125,6 @@ pub unsafe extern "C" fn rdx_keyword_get(name: *const c_char) -> *const CKeyword
1066
1125
  }
1067
1126
  }
1068
1127
 
1069
- /// The result of running a Cypher query, carrying either the formatted output or an error message.
1070
- #[repr(C)]
1071
- pub struct CQueryResult {
1072
- /// Non-null on success; null on error. Caller must free with `free_c_string`.
1073
- pub output: *const c_char,
1074
- /// Non-null on error; null on success. Caller must free with `free_c_string`.
1075
- pub error: *const c_char,
1076
- }
1077
-
1078
- impl CQueryResult {
1079
- fn success(output: &str) -> Self {
1080
- match CString::new(output) {
1081
- Ok(c_string) => Self {
1082
- output: c_string.into_raw().cast_const(),
1083
- error: ptr::null(),
1084
- },
1085
- Err(_) => Self::error("query output contained an interior NUL byte"),
1086
- }
1087
- }
1088
-
1089
- fn error(message: &str) -> Self {
1090
- Self {
1091
- output: ptr::null(),
1092
- error: CString::new(message).map_or(ptr::null(), |s| s.into_raw().cast_const()),
1093
- }
1094
- }
1095
- }
1096
-
1097
- /// The result of parsing a Cypher query into an opaque, reusable parsed-query object.
1098
- #[repr(C)]
1099
- pub struct CParseResult {
1100
- /// Non-null on success: a heap-allocated parsed query. Free with `rdx_cypher_query_free`.
1101
- pub query: *mut c_void,
1102
- /// Non-null on error; null on success. Caller must free with `free_c_string`.
1103
- pub error: *const c_char,
1104
- }
1105
-
1106
- /// Parses a Cypher query string into an opaque parsed-query object, without needing a graph.
1107
- ///
1108
- /// On success, `query` is a heap-allocated parsed query that can be executed against a graph with
1109
- /// `rdx_query_run` and must eventually be freed with `rdx_cypher_query_free`. On failure, `error`
1110
- /// holds the message.
1111
- ///
1112
- /// # Safety
1113
- ///
1114
- /// - `query` must be a valid, null-terminated UTF-8 string.
1115
- #[unsafe(no_mangle)]
1116
- pub unsafe extern "C" fn rdx_cypher_parse(query: *const c_char) -> CParseResult {
1117
- let Ok(query_str) = (unsafe { utils::convert_char_ptr_to_string(query) }) else {
1118
- return CParseResult {
1119
- query: ptr::null_mut(),
1120
- error: CString::new("query is not valid UTF-8").map_or(ptr::null(), |s| s.into_raw().cast_const()),
1121
- };
1122
- };
1123
-
1124
- match cypher::parse(&query_str) {
1125
- Ok(parsed) => CParseResult {
1126
- query: Box::into_raw(Box::new(parsed)).cast::<c_void>(),
1127
- error: ptr::null(),
1128
- },
1129
- Err(error) => CParseResult {
1130
- query: ptr::null_mut(),
1131
- error: CString::new(error.to_string()).map_or(ptr::null(), |s| s.into_raw().cast_const()),
1132
- },
1133
- }
1134
- }
1135
-
1136
- /// Frees a parsed query previously returned by `rdx_cypher_parse`.
1137
- ///
1138
- /// # Safety
1139
- ///
1140
- /// - `query` must be a pointer returned by `rdx_cypher_parse`, or null. It must not be used after.
1141
- #[unsafe(no_mangle)]
1142
- pub unsafe extern "C" fn rdx_cypher_query_free(query: *mut c_void) {
1143
- if query.is_null() {
1144
- return;
1145
- }
1146
- let _ = unsafe { Box::from_raw(query.cast::<cypher::Query>()) };
1147
- }
1148
-
1149
- /// Executes a previously parsed query (from `rdx_cypher_parse`) against the graph and returns the
1150
- /// formatted output or an error message. `format` must be `"table"` or `"json"`.
1151
- ///
1152
- /// # Safety
1153
- ///
1154
- /// - `query` must be a valid pointer returned by `rdx_cypher_parse`.
1155
- /// - `pointer` must be a valid `GraphPointer` previously returned by this crate.
1156
- /// - `format` must be a valid, null-terminated UTF-8 string.
1157
- #[unsafe(no_mangle)]
1158
- pub unsafe extern "C" fn rdx_query_run(
1159
- query: *const c_void,
1160
- pointer: GraphPointer,
1161
- format: *const c_char,
1162
- ) -> CQueryResult {
1163
- if query.is_null() {
1164
- return CQueryResult::error("query is null");
1165
- }
1166
-
1167
- let Ok(format_str) = (unsafe { utils::convert_char_ptr_to_string(format) }) else {
1168
- return CQueryResult::error("format is not valid UTF-8");
1169
- };
1170
-
1171
- let output_format = match format_str.as_str() {
1172
- "table" => OutputFormat::Table,
1173
- "json" => OutputFormat::Json,
1174
- other => {
1175
- return CQueryResult::error(&format!("unknown query format `{other}` (expected `table` or `json`)"));
1176
- }
1177
- };
1178
-
1179
- let parsed = unsafe { &*query.cast::<cypher::Query>() };
1180
-
1181
- with_graph(pointer, |graph| {
1182
- match cypher::run_parsed(graph, parsed, output_format) {
1183
- Ok(output) => CQueryResult::success(&output),
1184
- Err(error) => CQueryResult::error(&error.to_string()),
1185
- }
1186
- })
1187
- }
1188
-
1189
- /// Returns a description of the queryable Cypher schema (node labels, relationship types, and
1190
- /// properties) in the given format (`"table"` or `"json"`). The schema is static and requires no
1191
- /// graph. Caller must free the returned pointer with `free_c_string`.
1192
- ///
1193
- /// # Safety
1194
- ///
1195
- /// - `format` must be a valid, null-terminated UTF-8 string.
1196
- #[unsafe(no_mangle)]
1197
- pub unsafe extern "C" fn rdx_cypher_schema(format: *const c_char) -> *const c_char {
1198
- let format_str = unsafe { utils::convert_char_ptr_to_string(format) }.unwrap_or_else(|_| "table".to_string());
1199
- let output_format = if format_str == "json" {
1200
- OutputFormat::Json
1201
- } else {
1202
- OutputFormat::Table
1203
- };
1204
-
1205
- CString::new(cypher::schema(output_format)).map_or(ptr::null(), |s| s.into_raw().cast_const())
1206
- }
1207
-
1208
1128
  #[repr(u8)]
1209
1129
  #[derive(Debug, Clone, Copy)]
1210
1130
  pub enum CVisibility {
@@ -68,6 +68,8 @@ macro_rules! iterator {
68
68
  };
69
69
  }
70
70
 
71
+ pub mod config_api;
72
+ pub mod cypher_api;
71
73
  pub mod declaration_api;
72
74
  pub mod definition_api;
73
75
  pub mod diagnostic_api;