rubydex 0.2.8 → 0.3.0

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -3
  3. data/THIRD_PARTY_LICENSES.html +168 -1381
  4. data/exe/rdx +136 -34
  5. data/ext/rubydex/declaration.c +1 -1
  6. data/ext/rubydex/definition.c +32 -4
  7. data/ext/rubydex/extconf.rb +0 -8
  8. data/ext/rubydex/graph.c +29 -18
  9. data/ext/rubydex/query.c +105 -0
  10. data/ext/rubydex/query.h +8 -0
  11. data/ext/rubydex/reference.c +60 -0
  12. data/ext/rubydex/rubydex.c +2 -0
  13. data/ext/rubydex/utils.c +12 -0
  14. data/ext/rubydex/utils.h +5 -0
  15. data/lib/rubydex/mcp_server/protocol.rb +156 -0
  16. data/lib/rubydex/mcp_server/tools/base_tool.rb +109 -0
  17. data/lib/rubydex/mcp_server/tools/codebase_stats_tool.rb +30 -0
  18. data/lib/rubydex/mcp_server/tools/find_constant_references_tool.rb +46 -0
  19. data/lib/rubydex/mcp_server/tools/get_declaration_tool.rb +68 -0
  20. data/lib/rubydex/mcp_server/tools/get_descendants_tool.rb +46 -0
  21. data/lib/rubydex/mcp_server/tools/get_file_declarations_tool.rb +55 -0
  22. data/lib/rubydex/mcp_server/tools/search_declarations_tool.rb +55 -0
  23. data/lib/rubydex/mcp_server.rb +219 -0
  24. data/lib/rubydex/version.rb +1 -1
  25. data/rbi/rubydex.rbi +26 -4
  26. data/rust/Cargo.lock +5 -552
  27. data/rust/Cargo.toml +0 -1
  28. data/rust/rubydex/Cargo.toml +1 -0
  29. data/rust/rubydex/benches/graph_memory.rs +22 -4
  30. data/rust/rubydex/src/compile_assertions.rs +15 -0
  31. data/rust/rubydex/src/config.rs +54 -34
  32. data/rust/rubydex/src/diagnostic.rs +1 -1
  33. data/rust/rubydex/src/indexing/rbs_indexer.rs +14 -2
  34. data/rust/rubydex/src/indexing/ruby_indexer.rs +49 -58
  35. data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +72 -16
  36. data/rust/rubydex/src/listing.rs +159 -102
  37. data/rust/rubydex/src/main.rs +3 -125
  38. data/rust/rubydex/src/model/declaration.rs +0 -11
  39. data/rust/rubydex/src/model/definitions.rs +27 -26
  40. data/rust/rubydex/src/model/document.rs +43 -7
  41. data/rust/rubydex/src/model/graph.rs +47 -35
  42. data/rust/rubydex/src/model/id.rs +55 -0
  43. data/rust/rubydex/src/model/ids.rs +21 -9
  44. data/rust/rubydex/src/model/name.rs +35 -7
  45. data/rust/rubydex/src/model/references.rs +16 -13
  46. data/rust/rubydex/src/operation/applier.rs +0 -2
  47. data/rust/rubydex/src/operation/ruby_builder.rs +48 -59
  48. data/rust/rubydex/src/query/cypher/schema.rs +790 -0
  49. data/rust/rubydex/src/query/cypher/schema_info.rs +161 -0
  50. data/rust/rubydex/src/query/cypher/tests.rs +228 -0
  51. data/rust/rubydex/src/query/cypher.rs +57 -0
  52. data/rust/rubydex/src/query.rs +2 -0
  53. data/rust/rubydex/src/resolution.rs +248 -227
  54. data/rust/rubydex/src/resolution_tests.rs +263 -65
  55. data/rust/rubydex-sys/src/declaration_api.rs +6 -3
  56. data/rust/rubydex-sys/src/definition_api.rs +27 -7
  57. data/rust/rubydex-sys/src/graph_api.rs +175 -27
  58. data/rust/rubydex-sys/src/reference_api.rs +58 -12
  59. metadata +17 -10
  60. data/exe/rubydex_mcp +0 -17
  61. data/lib/rubydex/bin/rubydex_mcp.exe +0 -0
  62. data/rust/rubydex-mcp/Cargo.toml +0 -34
  63. data/rust/rubydex-mcp/src/main.rs +0 -48
  64. data/rust/rubydex-mcp/src/server.rs +0 -1148
  65. data/rust/rubydex-mcp/src/tools.rs +0 -49
  66. data/rust/rubydex-mcp/tests/mcp.rs +0 -302
data/exe/rdx CHANGED
@@ -5,49 +5,151 @@ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
5
 
6
6
  require "optparse"
7
7
 
8
- options = {}
8
+ USAGE = <<~TEXT
9
+ Usage: rdx <command> [options]
9
10
 
10
- OptionParser.new do |parser|
11
- parser.on("--version", "Print the gem's version") do
11
+ Commands:
12
+ query <CYPHER> Run a Cypher query against the workspace graph and print the result.
13
+ Use `query --schema` to describe the queryable schema (labels,
14
+ relationships, properties) without indexing the workspace.
15
+ console Open an interactive session with a populated graph for the current workspace
16
+ mcp [PATH] Run the MCP server for AI assistants (workspace defaults to the current dir)
17
+ help Show this help message
18
+
19
+ Run `rdx <command> --help` for command-specific options.
20
+ TEXT
21
+
22
+ def abort_with_usage(message)
23
+ warn(message)
24
+ warn("")
25
+ warn(USAGE)
26
+ exit(1)
27
+ end
28
+
29
+ # Top-level --version / --help / bare invocation, handled before command dispatch. Each branch
30
+ # performs its action and reports whether it handled the invocation, so we exit exactly once here
31
+ # rather than from inside every branch.
32
+ handled =
33
+ case ARGV.first
34
+ when "--version", "version"
12
35
  require "rubydex/version"
13
36
  puts "v#{Rubydex::VERSION}"
14
- exit
37
+ true
38
+ when nil, "-h", "--help", "help"
39
+ puts USAGE
40
+ true
41
+ else
42
+ false
15
43
  end
16
44
 
17
- parser.on("-h", "--help", "Prints this help") do
18
- puts parser
19
- exit
20
- end
45
+ exit if handled
21
46
 
22
- parser.on("-i", "--interactive", "Open an interactive session with a populated graph for the current workspace") do
23
- options[:interactive] = true
24
- end
25
- end.parse!
26
-
27
- require "rubydex"
47
+ command = ARGV.shift
28
48
 
29
- def __with_timer(message, &block)
30
- print(message)
49
+ def with_timer(io, message)
50
+ io.print(message)
31
51
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
32
- block.call
52
+ yield
33
53
  duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start
34
- puts " finished in #{duration.round(2)}ms"
54
+ io.puts(" finished in #{duration.round(2)}ms")
35
55
  end
36
56
 
37
- graph = Rubydex::Graph.new
38
- graph.load_config
39
-
40
- __with_timer("Indexing workspace...") { graph.index_workspace }
41
- __with_timer("Resolving graph...") { graph.resolve }
42
-
43
- if options[:interactive]
44
- begin
45
- require "irb"
46
- IRB.setup(nil)
47
- IRB.conf[:IRB_NAME] = "rubydex"
48
- workspace = IRB::WorkSpace.new(binding)
49
- IRB::Irb.new(workspace).run(IRB.conf)
50
- rescue LoadError
51
- abort("Interactive mode requires `irb` to be in the bundle")
52
- end
57
+ # Builds the workspace graph, sending progress messages to `progress_io`.
58
+ def build_graph(progress_io)
59
+ graph = Rubydex::Graph.new
60
+ graph.load_config
61
+ with_timer(progress_io, "Indexing workspace...") { graph.index_workspace }
62
+ with_timer(progress_io, "Resolving graph...") { graph.resolve }
63
+ graph
53
64
  end
65
+
66
+ require "rubydex"
67
+
68
+ # Resolve the command into an operation on a populated graph. Each command parses its own options
69
+ # and does any graph-independent work here; a command that needs no graph (like `query --schema`)
70
+ # handles itself and exits. Whatever falls through returns a lambda that runs against the graph.
71
+ operation =
72
+ case command
73
+ when "query"
74
+ schema = false
75
+ format = "table"
76
+ OptionParser.new do |parser|
77
+ parser.banner = "Usage: rdx query <CYPHER> [options]"
78
+ parser.on("--schema", "Describe the queryable schema instead of running a query") { schema = true }
79
+ parser.on("--format FORMAT", ["table", "json"], "Output format (table or json)") { |value| format = value }
80
+ parser.on("-h", "--help", "Show this help") do
81
+ puts parser
82
+ exit
83
+ end
84
+ end.parse!
85
+
86
+ query = ARGV.shift
87
+
88
+ if schema
89
+ warn("warning: ignoring query argument because `--schema` was given") if query
90
+ # The schema is static, so describe it without building a graph, then exit.
91
+ print(Rubydex::Query.schema(format))
92
+ exit
93
+ end
94
+
95
+ abort_with_usage("`query` requires a Cypher query argument (or pass `--schema`)") if query.nil? || query.empty?
96
+
97
+ # Parse the query up front so a malformed query fails fast, before the expensive indexing below.
98
+ parsed = begin
99
+ Rubydex::Query.parse(query)
100
+ rescue ArgumentError => e
101
+ abort(e.message)
102
+ end
103
+
104
+ lambda do |graph|
105
+ print(parsed.render(graph, format))
106
+ rescue ArgumentError => e
107
+ abort(e.message)
108
+ end
109
+ when "console"
110
+ OptionParser.new do |parser|
111
+ parser.banner = "Usage: rdx console"
112
+ parser.on("-h", "--help", "Show this help") do
113
+ puts parser
114
+ exit
115
+ end
116
+ end.parse!
117
+
118
+ lambda do |graph|
119
+ require "irb"
120
+ IRB.setup(nil)
121
+ IRB.conf[:IRB_NAME] = "rubydex"
122
+ IRB::Irb.new(IRB::WorkSpace.new(binding)).run(IRB.conf)
123
+ rescue LoadError
124
+ abort("Interactive mode requires `irb` to be in the bundle")
125
+ end
126
+ when "mcp"
127
+ parser = OptionParser.new do |p|
128
+ p.banner = "Usage: rdx mcp [PATH]"
129
+ p.on("-h", "--help", "Show this help") do
130
+ puts p
131
+ exit
132
+ end
133
+ end
134
+ begin
135
+ parser.parse!
136
+ rescue OptionParser::ParseError => e
137
+ abort_with_usage(e.message)
138
+ end
139
+
140
+ path = ARGV.shift || Dir.pwd
141
+ abort_with_usage("unexpected argument: #{ARGV.first}") unless ARGV.empty?
142
+
143
+ # The MCP server manages its own graph lifecycle for the given workspace, so it runs here
144
+ # instead of falling through to the shared graph build below.
145
+ require "rubydex/mcp_server"
146
+ Rubydex::MCPServer.run(path)
147
+ exit
148
+ else
149
+ abort_with_usage("unknown command: #{command}")
150
+ end
151
+
152
+ # Everything that reaches here operates on a populated graph. Progress goes to stderr so stdout
153
+ # carries only the command's output (e.g. for piping a query's JSON).
154
+ graph = build_graph($stderr)
155
+ operation.call(graph)
@@ -236,7 +236,7 @@ static VALUE rdxr_declaration_owner(VALUE self) {
236
236
  const CDeclaration *decl = rdx_declaration_owner(graph, data->id);
237
237
 
238
238
  if (decl == NULL) {
239
- rb_raise(rb_eRuntimeError, "owner can never be nil for any declarations");
239
+ rb_raise(rb_eRuntimeError, "Declaration must exist for a valid id");
240
240
  }
241
241
 
242
242
  VALUE decl_class = rdxi_declaration_class_for_kind(decl->kind);
@@ -18,6 +18,7 @@ static VALUE mRubydex;
18
18
  static VALUE cInclude;
19
19
  static VALUE cPrepend;
20
20
  static VALUE cExtend;
21
+ static VALUE cDocument;
21
22
  VALUE cComment;
22
23
  VALUE cDefinition;
23
24
  VALUE cClassDefinition;
@@ -88,6 +89,9 @@ static VALUE rdxr_definition_location(VALUE self) {
88
89
  void *graph = rdxi_graph_from_handle(self, &data);
89
90
 
90
91
  Location *loc = rdx_definition_location(graph, data->id);
92
+ if (loc == NULL) {
93
+ rb_raise(rb_eRuntimeError, "Definition must exist for a valid id");
94
+ }
91
95
  VALUE location = rdxi_build_location_value(loc);
92
96
  rdx_location_free(loc);
93
97
 
@@ -105,10 +109,11 @@ static VALUE rdxr_definition_comments(VALUE self) {
105
109
  void *graph = rdxi_graph_from_handle(self, &data);
106
110
 
107
111
  CommentArray *arr = rdx_definition_comments(graph, data->id);
108
- if (arr == NULL || arr->len == 0) {
109
- if (arr != NULL) {
110
- rdx_definition_comments_free(arr);
111
- }
112
+ if (arr == NULL) {
113
+ rb_raise(rb_eRuntimeError, "Definition must exist for a valid id");
114
+ }
115
+ if (arr->len == 0) {
116
+ rdx_definition_comments_free(arr);
112
117
  return rb_ary_new();
113
118
  }
114
119
 
@@ -261,6 +266,27 @@ static VALUE rdxr_definition_lexical_nesting(VALUE self) {
261
266
  return nesting;
262
267
  }
263
268
 
269
+ /*
270
+ * call-seq:
271
+ * document -> Rubydex::Document
272
+ *
273
+ * Returns the document this definition belongs to.
274
+ */
275
+ static VALUE rdxr_definition_document(VALUE self) {
276
+ HandleData *data;
277
+ void *graph = rdxi_graph_from_handle(self, &data);
278
+
279
+ const uint64_t *uri_id = rdx_definition_document(graph, data->id);
280
+ if (uri_id == NULL) {
281
+ rb_raise(rb_eRuntimeError, "Definition not found");
282
+ }
283
+
284
+ VALUE argv[] = {data->graph_obj, ULL2NUM(*uri_id)};
285
+ free_u64(uri_id);
286
+
287
+ return rb_class_new_instance(2, argv, cDocument);
288
+ }
289
+
264
290
  static VALUE rdxi_build_constant_reference(VALUE graph_obj, const CConstantReference *cref) {
265
291
  VALUE ref_class = (cref->declaration_id == 0)
266
292
  ? cUnresolvedConstantReference
@@ -397,6 +423,7 @@ void rdxi_initialize_definition(VALUE mod) {
397
423
  cInclude = rb_const_get(mRubydex, rb_intern("Include"));
398
424
  cPrepend = rb_const_get(mRubydex, rb_intern("Prepend"));
399
425
  cExtend = rb_const_get(mRubydex, rb_intern("Extend"));
426
+ cDocument = rb_const_get(mRubydex, rb_intern("Document"));
400
427
 
401
428
  cComment = rb_define_class_under(mRubydex, "Comment", rb_cObject);
402
429
 
@@ -412,6 +439,7 @@ void rdxi_initialize_definition(VALUE mod) {
412
439
  rb_define_method(cDefinition, "declaration", rdxr_definition_declaration, 0);
413
440
  rb_define_method(cDefinition, "lexical_owner", rdxr_definition_lexical_owner, 0);
414
441
  rb_define_method(cDefinition, "lexical_nesting", rdxr_definition_lexical_nesting, 0);
442
+ rb_define_method(cDefinition, "document", rdxr_definition_document, 0);
415
443
 
416
444
  cClassDefinition = rb_define_class_under(mRubydex, "ClassDefinition", cDefinition);
417
445
  rb_define_method(cClassDefinition, "superclass", rdxr_class_definition_superclass, 0);
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "mkmf"
4
- require "fileutils"
5
4
  require "pathname"
6
5
 
7
6
  unless system("cargo", "--version", out: File::NULL, err: File::NULL)
@@ -82,12 +81,6 @@ else
82
81
  end
83
82
 
84
83
  lib_dir = gem_dir.join("lib").join("rubydex")
85
- mcp_bin_dir = lib_dir.join("bin")
86
- FileUtils.mkdir_p(mcp_bin_dir)
87
-
88
- mcp_executable = Gem.win_platform? ? "rubydex_mcp.exe" : "rubydex_mcp"
89
- mcp_src = target_dir.join(mcp_executable)
90
- mcp_dst = mcp_bin_dir.join(mcp_executable)
91
84
 
92
85
  copy_dylib_commands = if Gem.win_platform?
93
86
  ""
@@ -114,7 +107,6 @@ new_makefile = makefile.gsub("$(OBJS): $(HDRS) $(ruby_headers)", <<~MAKEFILE.cho
114
107
  .rust_built: $(RUST_SRCS)
115
108
  \t#{cargo_command} || (echo "Compiling Rust failed" && exit 1)
116
109
  \t$(COPY) #{bindings_path} #{__dir__}
117
- \t$(COPY) #{mcp_src} #{mcp_dst}
118
110
  \ttouch $@
119
111
 
120
112
  compile_rust: .rust_built
data/ext/rubydex/graph.c CHANGED
@@ -63,16 +63,25 @@ const rb_data_type_t graph_type = {
63
63
  },
64
64
  .parent = NULL,
65
65
  .data = NULL,
66
- .flags = RUBY_TYPED_FREE_IMMEDIATELY,
66
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_FROZEN_SHAREABLE,
67
67
  };
68
68
 
69
- // Custom allocator for the Graph class. Calls into Rust to create a new `Arc<Mutex<Graph>>` that gets stored internally
70
- // as a void pointer
69
+ // Custom allocator for the Graph class. Calls into Rust to create a new Box<RwLock<Graph>> that gets stored
70
+ // internally as an opaque void pointer. The RwLock provides thread-safe concurrent access across Ractors.
71
71
  static VALUE rdxr_graph_alloc(VALUE klass) {
72
72
  void *graph = rdx_graph_new();
73
73
  return TypedData_Wrap_Struct(klass, &graph_type, graph);
74
74
  }
75
75
 
76
+ // Graph is intentionally non-copyable. A dup/clone of the TypedData wrapper would alias the
77
+ // same underlying Rust allocation (double-free on GC) or, if deep-copied, balloon memory use.
78
+ // Block both paths: `dup` dispatches to `initialize_copy`, `clone` to `initialize_clone`.
79
+ static VALUE rdxr_graph_initialize_copy(VALUE self, VALUE other) {
80
+ (void)self;
81
+ (void)other;
82
+ rb_raise(rb_eRuntimeError, "Rubydex::Graph cannot be duplicated or cloned");
83
+ }
84
+
76
85
  /*
77
86
  * call-seq:
78
87
  * index_all(file_paths) -> Array[String]
@@ -796,38 +805,38 @@ static VALUE rdxr_graph_complete_method_argument(int argc, VALUE *argv, VALUE se
796
805
 
797
806
  /*
798
807
  * call-seq:
799
- * exclude_paths(paths) -> nil
808
+ * exclude_patterns(patterns) -> nil
800
809
  *
801
- * Excludes the paths from file discovery during indexing.
810
+ * Excludes files matching the given glob patterns from discovery during indexing.
802
811
  */
803
- static VALUE rdxr_graph_exclude_paths(VALUE self, VALUE paths) {
804
- Check_Type(paths, T_ARRAY);
805
- rdxi_check_array_of_strings(paths);
812
+ static VALUE rdxr_graph_exclude_patterns(VALUE self, VALUE patterns) {
813
+ Check_Type(patterns, T_ARRAY);
814
+ rdxi_check_array_of_strings(patterns);
806
815
 
807
- size_t length = RARRAY_LEN(paths);
808
- char **converted_paths = rdxi_str_array_to_char(paths, length);
816
+ size_t length = RARRAY_LEN(patterns);
817
+ char **converted_patterns = rdxi_str_array_to_char(patterns, length);
809
818
 
810
819
  void *graph;
811
820
  TypedData_Get_Struct(self, void*, &graph_type, graph);
812
821
 
813
- rdx_graph_exclude_paths(graph, (const char **)converted_paths, length);
814
- rdxi_free_str_array(converted_paths, length);
822
+ rdx_graph_exclude_patterns(graph, (const char **)converted_patterns, length);
823
+ rdxi_free_str_array(converted_patterns, length);
815
824
 
816
825
  return Qnil;
817
826
  }
818
827
 
819
828
  /*
820
829
  * call-seq:
821
- * excluded_paths -> Array[String]
830
+ * excluded_patterns -> Array[String]
822
831
  *
823
- * Returns the paths currently excluded from file discovery.
832
+ * Returns the glob patterns currently excluded from file discovery.
824
833
  */
825
- static VALUE rdxr_graph_excluded_paths(VALUE self) {
834
+ static VALUE rdxr_graph_excluded_patterns(VALUE self) {
826
835
  void *graph;
827
836
  TypedData_Get_Struct(self, void*, &graph_type, graph);
828
837
 
829
838
  size_t out_count = 0;
830
- const char *const *results = rdx_graph_excluded_paths(graph, &out_count);
839
+ const char *const *results = rdx_graph_excluded_patterns(graph, &out_count);
831
840
 
832
841
  if (results == NULL) {
833
842
  return rb_ary_new();
@@ -943,6 +952,8 @@ void rdxi_initialize_graph(VALUE moduleRubydex) {
943
952
  id_self_receiver = rb_intern("self_receiver");
944
953
 
945
954
  rb_define_alloc_func(cGraph, rdxr_graph_alloc);
955
+ rb_define_method(cGraph, "initialize_copy", rdxr_graph_initialize_copy, 1);
956
+ rb_define_method(cGraph, "initialize_clone", rdxr_graph_initialize_copy, 1);
946
957
  rb_define_method(cGraph, "index_all", rdxr_graph_index_all, 1);
947
958
  rb_define_method(cGraph, "index_source", rdxr_graph_index_source, 3);
948
959
  rb_define_method(cGraph, "document", rdxr_graph_document, 1);
@@ -965,8 +976,8 @@ void rdxi_initialize_graph(VALUE moduleRubydex) {
965
976
  rb_define_method(cGraph, "complete_namespace_access", rdxr_graph_complete_namespace_access, -1);
966
977
  rb_define_method(cGraph, "complete_method_call", rdxr_graph_complete_method_call, -1);
967
978
  rb_define_method(cGraph, "complete_method_argument", rdxr_graph_complete_method_argument, -1);
968
- rb_define_method(cGraph, "exclude_paths", rdxr_graph_exclude_paths, 1);
969
- rb_define_method(cGraph, "excluded_paths", rdxr_graph_excluded_paths, 0);
979
+ rb_define_method(cGraph, "exclude_patterns", rdxr_graph_exclude_patterns, 1);
980
+ rb_define_method(cGraph, "excluded_patterns", rdxr_graph_excluded_patterns, 0);
970
981
  rb_define_method(cGraph, "workspace_path", rdxr_graph_workspace_path, 0);
971
982
  rb_define_method(cGraph, "workspace_path=", rdxr_graph_set_workspace_path, 1);
972
983
  rb_define_method(cGraph, "load_config", rdxr_graph_load_config, -1);
@@ -0,0 +1,105 @@
1
+ #include "query.h"
2
+ #include "graph.h"
3
+ #include "rustbindings.h"
4
+ #include "utils.h"
5
+
6
+ /*
7
+ * call-seq:
8
+ * Rubydex::Query.schema(format = :table) -> String
9
+ *
10
+ * Returns a description of the queryable Cypher schema. +format+ may be +:table+ (default) or
11
+ * +:json+. The schema is static, so it does not require a graph.
12
+ */
13
+ static VALUE rdxr_cypher_schema(int argc, VALUE *argv, VALUE self) {
14
+ VALUE format;
15
+ rb_scan_args(argc, argv, "01", &format);
16
+
17
+ const char *output = rdx_cypher_schema(rdxi_symbol_or_string_cstr(format, "table"));
18
+ VALUE result = output == NULL ? rb_utf8_str_new_cstr("") : rb_utf8_str_new_cstr(output);
19
+ if (output != NULL) {
20
+ free_c_string(output);
21
+ }
22
+
23
+ return result;
24
+ }
25
+
26
+ // Free function for Rubydex::Query: releases the parsed query allocated by Rust.
27
+ static void query_free(void *ptr) {
28
+ if (ptr) {
29
+ rdx_cypher_query_free(ptr);
30
+ }
31
+ }
32
+
33
+ static const rb_data_type_t query_type = {
34
+ .wrap_struct_name = "Rubydex::Query",
35
+ .function = {
36
+ .dmark = NULL,
37
+ .dfree = query_free,
38
+ .dsize = NULL,
39
+ .dcompact = NULL,
40
+ },
41
+ .parent = NULL,
42
+ .data = NULL,
43
+ .flags = RUBY_TYPED_FREE_IMMEDIATELY,
44
+ };
45
+
46
+ /*
47
+ * call-seq:
48
+ * Rubydex::Query.parse(query) -> Rubydex::Query
49
+ *
50
+ * Parses a Cypher query into an opaque, reusable object without needing a graph. Raises
51
+ * ArgumentError on a syntax error, so a query can be validated before building a graph.
52
+ */
53
+ static VALUE rdxr_query_parse(VALUE klass, VALUE query) {
54
+ Check_Type(query, T_STRING);
55
+
56
+ struct CParseResult result = rdx_cypher_parse(StringValueCStr(query));
57
+ if (result.error != NULL) {
58
+ VALUE message = rb_utf8_str_new_cstr(result.error);
59
+ free_c_string(result.error);
60
+ rb_raise(rb_eArgError, "%s", StringValueCStr(message));
61
+ }
62
+
63
+ return TypedData_Wrap_Struct(klass, &query_type, result.query);
64
+ }
65
+
66
+ /*
67
+ * call-seq:
68
+ * render(graph, format = :table) -> String
69
+ *
70
+ * Runs this parsed query against +graph+ and returns the formatted output. +format+ may be
71
+ * +:table+ (default) or +:json+. Raises ArgumentError on an execution or format error.
72
+ */
73
+ static VALUE rdxr_query_render(int argc, VALUE *argv, VALUE self) {
74
+ VALUE graph_obj, format;
75
+ rb_scan_args(argc, argv, "11", &graph_obj, &format);
76
+
77
+ void *query;
78
+ TypedData_Get_Struct(self, void *, &query_type, query);
79
+
80
+ void *graph;
81
+ TypedData_Get_Struct(graph_obj, void *, &graph_type, graph);
82
+
83
+ struct CQueryResult result = rdx_query_run(query, graph, rdxi_symbol_or_string_cstr(format, "table"));
84
+
85
+ if (result.error != NULL) {
86
+ VALUE message = rb_utf8_str_new_cstr(result.error);
87
+ free_c_string(result.error);
88
+ rb_raise(rb_eArgError, "%s", StringValueCStr(message));
89
+ }
90
+
91
+ VALUE output = result.output == NULL ? rb_utf8_str_new_cstr("") : rb_utf8_str_new_cstr(result.output);
92
+ if (result.output != NULL) {
93
+ free_c_string(result.output);
94
+ }
95
+
96
+ return output;
97
+ }
98
+
99
+ void rdxi_initialize_query(VALUE mRubydex) {
100
+ VALUE cQuery = rb_define_class_under(mRubydex, "Query", rb_cObject);
101
+ rb_undef_alloc_func(cQuery);
102
+ rb_define_singleton_method(cQuery, "parse", rdxr_query_parse, 1);
103
+ rb_define_singleton_method(cQuery, "schema", rdxr_cypher_schema, -1);
104
+ rb_define_method(cQuery, "render", rdxr_query_render, -1);
105
+ }
@@ -0,0 +1,8 @@
1
+ #ifndef RUBYDEX_QUERY_H
2
+ #define RUBYDEX_QUERY_H
3
+
4
+ #include "ruby.h"
5
+
6
+ void rdxi_initialize_query(VALUE mRubydex);
7
+
8
+ #endif // RUBYDEX_QUERY_H
@@ -17,6 +17,8 @@ VALUE cUnresolvedConstantReference;
17
17
  VALUE cResolvedConstantReference;
18
18
  VALUE cMethodReference;
19
19
 
20
+ static VALUE cDocument;
21
+
20
22
  /*
21
23
  * call-seq:
22
24
  * name -> String
@@ -28,6 +30,9 @@ static VALUE rdxr_constant_reference_name(VALUE self) {
28
30
  void *graph = rdxi_graph_from_handle(self, &data);
29
31
 
30
32
  const char *name = rdx_constant_reference_name(graph, data->id);
33
+ if (name == NULL) {
34
+ rb_raise(rb_eRuntimeError, "Constant reference must exist for a valid id");
35
+ }
31
36
  return rdxi_owned_c_string_to_ruby(name);
32
37
  }
33
38
 
@@ -42,11 +47,35 @@ static VALUE rdxr_constant_reference_location(VALUE self) {
42
47
  void *graph = rdxi_graph_from_handle(self, &data);
43
48
 
44
49
  Location *loc = rdx_constant_reference_location(graph, data->id);
50
+ if (loc == NULL) {
51
+ rb_raise(rb_eRuntimeError, "Constant reference must exist for a valid id");
52
+ }
45
53
  VALUE location = rdxi_build_location_value(loc);
46
54
  rdx_location_free(loc);
47
55
  return location;
48
56
  }
49
57
 
58
+ /*
59
+ * call-seq:
60
+ * document -> Rubydex::Document
61
+ *
62
+ * Returns the document this constant reference belongs to.
63
+ */
64
+ static VALUE rdxr_constant_reference_document(VALUE self) {
65
+ HandleData *data;
66
+ void *graph = rdxi_graph_from_handle(self, &data);
67
+
68
+ const uint64_t *uri_id = rdx_constant_reference_document(graph, data->id);
69
+ if (uri_id == NULL) {
70
+ rb_raise(rb_eRuntimeError, "Constant reference not found");
71
+ }
72
+
73
+ VALUE argv[] = {data->graph_obj, ULL2NUM(*uri_id)};
74
+ free_u64(uri_id);
75
+
76
+ return rb_class_new_instance(2, argv, cDocument);
77
+ }
78
+
50
79
  /*
51
80
  * call-seq:
52
81
  * name -> String
@@ -58,6 +87,9 @@ static VALUE rdxr_method_reference_name(VALUE self) {
58
87
  void *graph = rdxi_graph_from_handle(self, &data);
59
88
 
60
89
  const char *name = rdx_method_reference_name(graph, data->id);
90
+ if (name == NULL) {
91
+ rb_raise(rb_eRuntimeError, "Method reference must exist for a valid id");
92
+ }
61
93
  return rdxi_owned_c_string_to_ruby(name);
62
94
  }
63
95
 
@@ -72,6 +104,9 @@ static VALUE rdxr_method_reference_location(VALUE self) {
72
104
  void *graph = rdxi_graph_from_handle(self, &data);
73
105
 
74
106
  Location *loc = rdx_method_reference_location(graph, data->id);
107
+ if (loc == NULL) {
108
+ rb_raise(rb_eRuntimeError, "Method reference must exist for a valid id");
109
+ }
75
110
  VALUE location = rdxi_build_location_value(loc);
76
111
  rdx_location_free(loc);
77
112
  return location;
@@ -100,6 +135,27 @@ static VALUE rdxr_method_reference_receiver(VALUE self) {
100
135
  return rb_class_new_instance(2, argv, decl_class);
101
136
  }
102
137
 
138
+ /*
139
+ * call-seq:
140
+ * document -> Rubydex::Document
141
+ *
142
+ * Returns the document this method reference belongs to.
143
+ */
144
+ static VALUE rdxr_method_reference_document(VALUE self) {
145
+ HandleData *data;
146
+ void *graph = rdxi_graph_from_handle(self, &data);
147
+
148
+ const uint64_t *uri_id = rdx_method_reference_document(graph, data->id);
149
+ if (uri_id == NULL) {
150
+ rb_raise(rb_eRuntimeError, "Method reference not found");
151
+ }
152
+
153
+ VALUE argv[] = {data->graph_obj, ULL2NUM(*uri_id)};
154
+ free_u64(uri_id);
155
+
156
+ return rb_class_new_instance(2, argv, cDocument);
157
+ }
158
+
103
159
  /*
104
160
  * call-seq:
105
161
  * declaration -> Rubydex::Declaration
@@ -123,6 +179,8 @@ static VALUE rdxr_resolved_constant_reference_declaration(VALUE self) {
123
179
  }
124
180
 
125
181
  void rdxi_initialize_reference(VALUE mRubydex) {
182
+ cDocument = rb_const_get(mRubydex, rb_intern("Document"));
183
+
126
184
  cReference = rb_define_class_under(mRubydex, "Reference", rb_cObject);
127
185
  rb_define_alloc_func(cReference, rdxr_handle_alloc);
128
186
  rb_define_method(cReference, "initialize", rdxr_handle_initialize, 2);
@@ -132,6 +190,7 @@ void rdxi_initialize_reference(VALUE mRubydex) {
132
190
  rb_define_alloc_func(cConstantReference, rdxr_handle_alloc);
133
191
  rb_define_method(cConstantReference, "initialize", rdxr_handle_initialize, 2);
134
192
  rb_define_method(cConstantReference, "location", rdxr_constant_reference_location, 0);
193
+ rb_define_method(cConstantReference, "document", rdxr_constant_reference_document, 0);
135
194
  rb_funcall(rb_singleton_class(cConstantReference), rb_intern("private"), 1, ID2SYM(rb_intern("new")));
136
195
 
137
196
  cUnresolvedConstantReference = rb_define_class_under(mRubydex, "UnresolvedConstantReference", cConstantReference);
@@ -148,4 +207,5 @@ void rdxi_initialize_reference(VALUE mRubydex) {
148
207
  rb_define_method(cMethodReference, "name", rdxr_method_reference_name, 0);
149
208
  rb_define_method(cMethodReference, "location", rdxr_method_reference_location, 0);
150
209
  rb_define_method(cMethodReference, "receiver", rdxr_method_reference_receiver, 0);
210
+ rb_define_method(cMethodReference, "document", rdxr_method_reference_document, 0);
151
211
  }
@@ -4,6 +4,7 @@
4
4
  #include "document.h"
5
5
  #include "graph.h"
6
6
  #include "location.h"
7
+ #include "query.h"
7
8
  #include "reference.h"
8
9
  #include "signature.h"
9
10
 
@@ -19,6 +20,7 @@ void Init_rubydex(void) {
19
20
  */
20
21
  mRubydex = rb_define_module("Rubydex");
21
22
  rdxi_initialize_graph(mRubydex);
23
+ rdxi_initialize_query(mRubydex);
22
24
  rdxi_initialize_declaration(mRubydex);
23
25
  rdxi_initialize_document(mRubydex);
24
26
  rdxi_initialize_definition(mRubydex);
data/ext/rubydex/utils.c CHANGED
@@ -52,6 +52,18 @@ VALUE rdxi_owned_c_string_to_ruby(const char *string) {
52
52
  return value;
53
53
  }
54
54
 
55
+ // Coerce an optional String, Symbol, or nil to a C string, defaulting to `default_value` for nil.
56
+ const char *rdxi_symbol_or_string_cstr(VALUE value, const char *default_value) {
57
+ if (NIL_P(value)) {
58
+ return default_value;
59
+ }
60
+ if (RB_TYPE_P(value, T_SYMBOL)) {
61
+ value = rb_sym2str(value);
62
+ }
63
+ Check_Type(value, T_STRING);
64
+ return StringValueCStr(value);
65
+ }
66
+
55
67
  // Yield body for iterating over declarations
56
68
  VALUE rdxi_declarations_yield(VALUE args) {
57
69
  VALUE self = rb_ary_entry(args, 0);