rubydex 0.2.9 → 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.
- checksums.yaml +4 -4
- data/README.md +69 -3
- data/THIRD_PARTY_LICENSES.html +34 -1
- data/exe/rdx +131 -55
- data/ext/rubydex/declaration.c +1 -1
- data/ext/rubydex/definition.c +32 -4
- data/ext/rubydex/graph.c +14 -3
- data/ext/rubydex/query.c +105 -0
- data/ext/rubydex/query.h +8 -0
- data/ext/rubydex/reference.c +60 -0
- data/ext/rubydex/rubydex.c +2 -0
- data/ext/rubydex/utils.c +12 -0
- data/ext/rubydex/utils.h +5 -0
- data/lib/rubydex/version.rb +1 -1
- data/rbi/rubydex.rbi +22 -0
- data/rust/Cargo.lock +7 -0
- data/rust/rubydex/Cargo.toml +1 -0
- data/rust/rubydex/benches/graph_memory.rs +22 -4
- data/rust/rubydex/src/compile_assertions.rs +15 -0
- data/rust/rubydex/src/diagnostic.rs +1 -1
- data/rust/rubydex/src/indexing/rbs_indexer.rs +14 -2
- data/rust/rubydex/src/indexing/ruby_indexer.rs +49 -58
- data/rust/rubydex/src/indexing/ruby_indexer_tests.rs +72 -16
- data/rust/rubydex/src/main.rs +2 -124
- data/rust/rubydex/src/model/declaration.rs +0 -11
- data/rust/rubydex/src/model/definitions.rs +27 -26
- data/rust/rubydex/src/model/document.rs +43 -7
- data/rust/rubydex/src/model/graph.rs +40 -28
- data/rust/rubydex/src/model/id.rs +55 -0
- data/rust/rubydex/src/model/ids.rs +21 -9
- data/rust/rubydex/src/model/name.rs +35 -7
- data/rust/rubydex/src/model/references.rs +16 -13
- data/rust/rubydex/src/operation/ruby_builder.rs +48 -59
- data/rust/rubydex/src/query/cypher/schema.rs +790 -0
- data/rust/rubydex/src/query/cypher/schema_info.rs +161 -0
- data/rust/rubydex/src/query/cypher/tests.rs +228 -0
- data/rust/rubydex/src/query/cypher.rs +57 -0
- data/rust/rubydex/src/query.rs +2 -0
- data/rust/rubydex/src/resolution.rs +248 -227
- data/rust/rubydex/src/resolution_tests.rs +263 -65
- data/rust/rubydex-sys/src/declaration_api.rs +6 -3
- data/rust/rubydex-sys/src/definition_api.rs +27 -7
- data/rust/rubydex-sys/src/graph_api.rs +158 -14
- data/rust/rubydex-sys/src/reference_api.rs +58 -12
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf823835a70dee735f2dc0e898eb1d47be47e4ab635a6be4de5677ceaf6bbe53
|
|
4
|
+
data.tar.gz: 663389c429d729bf8d9191f54b7eafe92b8ecb8e0c0a32e5b3f6adba3365f3fa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: aa17ed0f03e392c4ad1a2bf256d3472057d7a78a2baca596bb72f80c06d1fe36d7fa80878ddfabe740ca0b8d23f961d01afe9ab0211219e2f41b36b4587b819d
|
|
7
|
+
data.tar.gz: 86d70527d4bcee2b8e90bbebb33912e1f5a6bf1f7fe9ce6e1435e7fff918829ae2a8de692072ab34fca42a199f5cfce5a12e18f77b6f08d5b8ca274c3e8f83f4
|
data/README.md
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
This project is a high-performance static analysis toolkit for the Ruby language. The goal is to be a solid
|
|
4
4
|
foundation to power a variety of tools, such as type checkers, linters, language servers and more.
|
|
5
5
|
|
|
6
|
+
[Ruby API Documentation](https://shopify.github.io/rubydex/)
|
|
7
|
+
|
|
6
8
|
## Usage
|
|
7
9
|
|
|
8
10
|
Both Ruby and Rust APIs are made available through a gem and a crate, respectively. Here's a simple example
|
|
@@ -77,6 +79,70 @@ diagnostic.message
|
|
|
77
79
|
diagnostic.location
|
|
78
80
|
```
|
|
79
81
|
|
|
82
|
+
## Ractor Safety
|
|
83
|
+
|
|
84
|
+
A resolved `Rubydex::Graph` can be shared across Ractors without copying:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
graph = Rubydex::Graph.new
|
|
88
|
+
graph.index_workspace
|
|
89
|
+
graph.resolve
|
|
90
|
+
Ractor.make_shareable(graph)
|
|
91
|
+
|
|
92
|
+
# Worker Ractors can now read the graph in parallel
|
|
93
|
+
ractor = Ractor.new(graph) { |g| g["Foo"]&.name }
|
|
94
|
+
ractor.value
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Thread safety comes from an `RwLock` on the Rust side, **not** from Ruby's
|
|
98
|
+
`freeze`. This is a deliberate, but potentially surprising, choice:
|
|
99
|
+
|
|
100
|
+
- A frozen (or `make_shareable`'d) graph **can still be mutated** — methods like
|
|
101
|
+
`index_source`, `resolve`, `exclude_patterns`, and `encoding=` work on a frozen
|
|
102
|
+
graph. This supports interactive use cases (LSP/MCP) that need incremental
|
|
103
|
+
edits while worker Ractors read concurrently.
|
|
104
|
+
- Because the same underlying allocation is shared, callers are responsible for
|
|
105
|
+
ordering concurrent writes and reads, as with any shared mutable state across
|
|
106
|
+
Ractors.
|
|
107
|
+
- Graphs cannot be `dup`'d or `clone`'d; both raise `RuntimeError` to avoid
|
|
108
|
+
aliasing the Rust allocation (which would double-free on GC) or ballooning
|
|
109
|
+
memory with a deep copy.
|
|
110
|
+
|
|
111
|
+
## Querying with Cypher
|
|
112
|
+
|
|
113
|
+
Rubydex exposes the indexed graph through a read-only subset of the
|
|
114
|
+
[Cypher](https://opencypher.org/) query language. Only read clauses (`MATCH`,
|
|
115
|
+
`WHERE`, `RETURN`, ...) are supported; there is no way to mutate the graph.
|
|
116
|
+
|
|
117
|
+
From the command line:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Run a query against the current workspace
|
|
121
|
+
bundle exec rdx query "MATCH (c:Class)-[:DEFINES]->(m:Method) RETURN c.name, m.name"
|
|
122
|
+
|
|
123
|
+
# Render results as JSON instead of a table
|
|
124
|
+
bundle exec rdx query "MATCH (c:Class) RETURN c.name" --format json
|
|
125
|
+
|
|
126
|
+
# Describe the queryable schema (node labels and relationship types) without indexing
|
|
127
|
+
bundle exec rdx query --schema
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
From Ruby:
|
|
131
|
+
|
|
132
|
+
```ruby
|
|
133
|
+
graph = Rubydex::Graph.new
|
|
134
|
+
graph.index_workspace
|
|
135
|
+
graph.resolve
|
|
136
|
+
|
|
137
|
+
# Parse once, render against a graph as a table or JSON string
|
|
138
|
+
query = Rubydex::Query.parse("MATCH (c:Class) RETURN c.name")
|
|
139
|
+
puts query.render(graph, "table")
|
|
140
|
+
puts query.render(graph, "json")
|
|
141
|
+
|
|
142
|
+
# Describe the schema
|
|
143
|
+
puts Rubydex::Query.schema("table")
|
|
144
|
+
```
|
|
145
|
+
|
|
80
146
|
## MCP Server (Experimental)
|
|
81
147
|
|
|
82
148
|
Rubydex can run as an MCP (Model Context Protocol) server, enabling AI assistants
|
|
@@ -94,16 +160,16 @@ like Claude to semantically query your Ruby codebase.
|
|
|
94
160
|
bundle install
|
|
95
161
|
```
|
|
96
162
|
|
|
97
|
-
3. Configure your MCP client to run `bundle exec rdx
|
|
163
|
+
3. Configure your MCP client to run `bundle exec rdx mcp`.
|
|
98
164
|
|
|
99
165
|
Using Claude Code as an example:
|
|
100
166
|
```bash
|
|
101
|
-
claude mcp add --scope project rubydex -- bundle exec rdx
|
|
167
|
+
claude mcp add --scope project rubydex -- bundle exec rdx mcp
|
|
102
168
|
```
|
|
103
169
|
|
|
104
170
|
Using Codex as an example:
|
|
105
171
|
```bash
|
|
106
|
-
codex mcp add rubydex -- bundle exec rdx
|
|
172
|
+
codex mcp add rubydex -- bundle exec rdx mcp
|
|
107
173
|
```
|
|
108
174
|
|
|
109
175
|
Start your MCP client from that project directory. The MCP server indexes
|
data/THIRD_PARTY_LICENSES.html
CHANGED
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
<ul class="licenses-overview">
|
|
53
53
|
<li><a href="#Apache-2.0">Apache License 2.0</a> (116)</li>
|
|
54
54
|
<li><a href="#Unicode-3.0">Unicode License v3</a> (19)</li>
|
|
55
|
-
<li><a href="#MIT">MIT License</a> (
|
|
55
|
+
<li><a href="#MIT">MIT License</a> (17)</li>
|
|
56
56
|
<li><a href="#BSD-2-Clause">BSD 2-Clause "Simplified" License</a> (2)</li>
|
|
57
57
|
<li><a href="#BSD-3-Clause">BSD 3-Clause "New" or "Revised" License</a> (1)</li>
|
|
58
58
|
<li><a href="#BSL-1.0">Boost Software License 1.0</a> (1)</li>
|
|
@@ -2802,6 +2802,39 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
2802
2802
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
2803
2803
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
2804
2804
|
SOFTWARE.
|
|
2805
|
+
</pre>
|
|
2806
|
+
</li>
|
|
2807
|
+
<li class="license">
|
|
2808
|
+
<h3 id="MIT">MIT License</h3>
|
|
2809
|
+
<h4>Used by:</h4>
|
|
2810
|
+
<ul class="license-used-by">
|
|
2811
|
+
<li><a
|
|
2812
|
+
href=" https://github.com/Shopify/cypher-parser ">cypher-parser
|
|
2813
|
+
0.2.0</a></li>
|
|
2814
|
+
</ul>
|
|
2815
|
+
<pre class="license-text">The MIT License (MIT)
|
|
2816
|
+
|
|
2817
|
+
Copyright (c) 2025-present, Shopify Inc.
|
|
2818
|
+
|
|
2819
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
2820
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
2821
|
+
in the Software without restriction, including without limitation the rights
|
|
2822
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
2823
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
2824
|
+
furnished to do so, subject to the following conditions:
|
|
2825
|
+
|
|
2826
|
+
The above copyright notice and this permission notice shall be included in
|
|
2827
|
+
all copies or substantial portions of the Software.
|
|
2828
|
+
|
|
2829
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
2830
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
2831
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
2832
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
2833
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
2834
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
2835
|
+
THE SOFTWARE.
|
|
2836
|
+
|
|
2837
|
+
See the generated THIRD_PARTY_LICENSES.html file for third party licenses.
|
|
2805
2838
|
</pre>
|
|
2806
2839
|
</li>
|
|
2807
2840
|
<li class="license">
|
data/exe/rdx
CHANGED
|
@@ -5,75 +5,151 @@ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
|
5
5
|
|
|
6
6
|
require "optparse"
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
USAGE = <<~TEXT
|
|
9
|
+
Usage: rdx <command> [options]
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
37
|
+
true
|
|
38
|
+
when nil, "-h", "--help", "help"
|
|
39
|
+
puts USAGE
|
|
40
|
+
true
|
|
41
|
+
else
|
|
42
|
+
false
|
|
15
43
|
end
|
|
16
44
|
|
|
17
|
-
|
|
18
|
-
puts parser
|
|
19
|
-
exit
|
|
20
|
-
end
|
|
45
|
+
exit if handled
|
|
21
46
|
|
|
22
|
-
|
|
23
|
-
options[:interactive] = true
|
|
24
|
-
end
|
|
47
|
+
command = ARGV.shift
|
|
25
48
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
49
|
+
def with_timer(io, message)
|
|
50
|
+
io.print(message)
|
|
51
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
|
52
|
+
yield
|
|
53
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - start
|
|
54
|
+
io.puts(" finished in #{duration.round(2)}ms")
|
|
29
55
|
end
|
|
30
56
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
38
64
|
end
|
|
39
65
|
|
|
40
66
|
require "rubydex"
|
|
41
67
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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!
|
|
49
85
|
|
|
50
|
-
|
|
51
|
-
Rubydex::MCPServer.run(arguments.fetch(0, Dir.pwd))
|
|
52
|
-
exit
|
|
53
|
-
end
|
|
86
|
+
query = ARGV.shift
|
|
54
87
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
62
103
|
|
|
63
|
-
|
|
64
|
-
graph
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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}")
|
|
78
150
|
end
|
|
79
|
-
|
|
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)
|
data/ext/rubydex/declaration.c
CHANGED
|
@@ -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, "
|
|
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);
|
data/ext/rubydex/definition.c
CHANGED
|
@@ -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
|
|
109
|
-
|
|
110
|
-
|
|
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);
|
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
|
|
70
|
-
// as
|
|
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]
|
|
@@ -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);
|
data/ext/rubydex/query.c
ADDED
|
@@ -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
|
+
}
|