Skip to content

Commit 9306fe5

Browse files
committed
Prototype Cypher commands in the rdx console
Add `Rubydex::Console`, which backs `rdx console` with an IRB session that keeps `graph` in scope (for Ruby, e.g. `graph["Foo"]`) and registers two Cypher commands via IRB's modern command API: rubydex> query MATCH (n:Class|Module) RETURN n.name ORDER BY n.name rubydex> schema The `query` command takes the rest of the line verbatim, so the Cypher does not need to be valid Ruby or quoted. This is the clean, idiomatic "query mode": rather than swapping the REPL's evaluator, Cypher lines are prefixed with `query`. - Extract the console out of exe/rdx into lib/rubydex/console.rb; the query-running logic lives in Console.run_query so it is testable without driving IRB. - Treat IRB as a soft, runtime dependency rather than a gemspec one. IRB is a default gem, so it is not declared in the gemspec; the Cypher commands register only when the installed IRB exposes the command API (>= 1.13), and the console degrades to a plain Ruby session on older versions.
1 parent 7e08a7b commit 9306fe5

3 files changed

Lines changed: 181 additions & 5 deletions

File tree

exe/rdx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -108,11 +108,8 @@ when "console"
108108
graph = build_graph($stdout)
109109

110110
begin
111-
require "irb"
112-
IRB.setup(nil)
113-
IRB.conf[:IRB_NAME] = "rubydex"
114-
workspace = IRB::WorkSpace.new(binding)
115-
IRB::Irb.new(workspace).run(IRB.conf)
111+
require "rubydex/console"
112+
Rubydex::Console.start(graph)
116113
rescue LoadError
117114
abort("Interactive mode requires `irb` to be in the bundle")
118115
end

lib/rubydex/console.rb

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# frozen_string_literal: true
2+
3+
require "irb"
4+
5+
module Rubydex
6+
# Interactive console backing `rdx console`.
7+
#
8+
# On top of a normal IRB session (with `graph` in scope so you can call Ruby directly, e.g.
9+
# `graph["Foo"]`), it registers commands that run Cypher against the same graph without needing
10+
# valid Ruby syntax or quoting:
11+
#
12+
# rubydex(main):001> query MATCH (n:Class|Module) RETURN n.name ORDER BY n.name
13+
# rubydex(main):002> schema
14+
#
15+
# The `query` command takes the rest of the line verbatim, so the Cypher does not have to parse as
16+
# Ruby. This is the clean, idiomatic realization of a "query mode": instead of swapping the REPL's
17+
# evaluator, Cypher lines are simply prefixed with `query`.
18+
#
19+
# The Cypher commands rely on IRB's command-registration API (IRB >= 1.13). On older IRB the
20+
# console still works as a plain Ruby session; only the `query`/`schema` shortcuts are unavailable.
21+
module Console
22+
class << self
23+
# The graph that the `query`/`schema` console commands operate on. Set by {.start}.
24+
attr_accessor :graph
25+
26+
# Starts an interactive session. `graph` is exposed both as a local variable (for the Ruby
27+
# side of the console) and to the Cypher commands via {.graph}.
28+
def start(graph)
29+
self.graph = graph
30+
31+
IRB.setup(nil)
32+
IRB.conf[:IRB_NAME] = "rubydex"
33+
IRB::Irb.new(IRB::WorkSpace.new(session_binding(graph))).run(IRB.conf)
34+
end
35+
36+
# Runs a Cypher query against {.graph} and returns the formatted output, or a human-readable
37+
# error message on failure. Extracted from the command so it can be exercised without IRB.
38+
def run_query(cypher, format: :table)
39+
cypher = cypher.to_s.strip
40+
return "Usage: query <CYPHER>" if cypher.empty?
41+
42+
Rubydex::Query.parse(cypher).render(graph, format)
43+
rescue ArgumentError => e
44+
e.message
45+
end
46+
47+
# Returns the queryable Cypher schema description.
48+
def describe_schema(format: :table)
49+
Rubydex::Query.schema(format)
50+
end
51+
52+
private
53+
54+
# A binding that exposes `graph` as a local variable for the Ruby side of the console.
55+
def session_binding(graph)
56+
binding
57+
end
58+
end
59+
end
60+
end
61+
62+
# Register the Cypher commands only when the installed IRB exposes the command API (>= 1.13).
63+
# Otherwise stop loading here: the console still works as a plain Ruby session, just without the
64+
# `query`/`schema` shortcuts.
65+
begin
66+
require "irb/command"
67+
return unless defined?(IRB::Command::Base) && IRB::Command.respond_to?(:register)
68+
rescue LoadError
69+
return
70+
end
71+
72+
module Rubydex
73+
module Console
74+
# `query <CYPHER>` — runs a Cypher query against the console graph and prints the result.
75+
class QueryCommand < IRB::Command::Base
76+
category "Rubydex"
77+
description "Run a Cypher query against the graph: query <CYPHER>"
78+
79+
def execute(arg)
80+
puts(Console.run_query(arg))
81+
nil
82+
end
83+
end
84+
85+
# `schema` — prints the queryable Cypher schema (labels, relationships, properties).
86+
class SchemaCommand < IRB::Command::Base
87+
category "Rubydex"
88+
description "Describe the queryable Cypher schema"
89+
90+
def execute(_arg)
91+
puts(Console.describe_schema)
92+
nil
93+
end
94+
end
95+
end
96+
end
97+
98+
IRB::Command.register(:query, Rubydex::Console::QueryCommand)
99+
IRB::Command.register(:schema, Rubydex::Console::SchemaCommand)

test/console_test.rb

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# frozen_string_literal: true
2+
3+
require "test_helper"
4+
require "helpers/context"
5+
require "json"
6+
require "rubydex/console"
7+
8+
class ConsoleTest < Minitest::Test
9+
include Test::Helpers::WithContext
10+
11+
def teardown
12+
Rubydex::Console.graph = nil
13+
end
14+
15+
def test_run_query_renders_table_against_the_console_graph
16+
with_graph("class Animal; end\nclass Dog < Animal; end\n") do
17+
output = Rubydex::Console.run_query("MATCH (c:Class)-[:INHERITS]->(p) WHERE c.name = 'Dog' RETURN p.name")
18+
19+
assert_match(/p\.name/, output)
20+
assert_match(/Animal/, output)
21+
end
22+
end
23+
24+
def test_run_query_supports_json_format
25+
with_graph("class Dog; end\n") do
26+
output = Rubydex::Console.run_query("MATCH (c:Class {name: 'Dog'}) RETURN c.name", format: :json)
27+
28+
assert_equal("[{\"c.name\":\"Dog\"}]", output)
29+
end
30+
end
31+
32+
def test_run_query_returns_usage_for_blank_input
33+
with_graph("class Dog; end\n") do
34+
assert_equal("Usage: query <CYPHER>", Rubydex::Console.run_query(" "))
35+
end
36+
end
37+
38+
def test_run_query_returns_error_message_on_invalid_query
39+
with_graph("class Dog; end\n") do
40+
output = Rubydex::Console.run_query("MATCH (c RETURN c")
41+
42+
assert_match(/Cypher syntax error/, output)
43+
end
44+
end
45+
46+
def test_describe_schema_lists_relationships
47+
output = Rubydex::Console.describe_schema(format: :json)
48+
parsed = JSON.parse(output)
49+
50+
assert(parsed["relationships"].any? { |r| r["type"] == "INHERITS" })
51+
end
52+
53+
def test_commands_are_registered_on_modern_irb
54+
names = IRB::Command.commands.keys
55+
56+
assert_includes(names, :query)
57+
assert_includes(names, :schema)
58+
end
59+
60+
def test_query_runs_without_the_command_api
61+
# The core query path must not depend on IRB's command registration, so the console still works
62+
# (minus the `query`/`schema` shortcuts) on older IRB versions.
63+
with_graph("class Dog; end\n") do
64+
assert_equal("[{\"c.name\":\"Dog\"}]", Rubydex::Console.run_query("MATCH (c:Class {name: 'Dog'}) RETURN c.name", format: :json))
65+
end
66+
end
67+
68+
private
69+
70+
def with_graph(source)
71+
with_context do |context|
72+
context.write!("zoo.rb", source)
73+
graph = Rubydex::Graph.new
74+
graph.index_all(context.glob("**/*.rb"))
75+
graph.resolve
76+
Rubydex::Console.graph = graph
77+
yield
78+
end
79+
end
80+
end

0 commit comments

Comments
 (0)