Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make detail tuples typed-ish #837

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
86 changes: 86 additions & 0 deletions spec/metadata_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
require "./spec_helper"

module LavinMQ
struct Metadata(T)
describe Value do
describe "compare" do
{% begin %}
{%
testdata = {
{1, 2, -1},
{2, 1, 1},
{"a", "b", -1},
{"b", "a", 1},
{1, 1, 0},
{1, nil, 1},
{nil, 1, -1},
{nil, nil, 0},
{1, "a", -1},
{"a", 1, 1},
}
%}
{% for values in testdata %}
{% left, right, expected = values %}
it "{{left.id}} <=> {{right.id}} is {{expected}}" do
res = Value.new({{left}}) <=> Value.new({{right}})
res.should eq {{expected}}
end
{% end %}
{% end %}
end
end
end

describe Metadata do
data = Metadata.new({
foo: "bar",
baz: 1,
sub: {
bar: "foo",
next: {
value: 2,
},
},
})

describe "#dig" do
it "can return first level value" do
data.dig("foo").should eq Metadata::Value.new("bar")
end

it "can return value from deep level" do
data.dig("sub.next.value").should eq Metadata::Value.new(2)
end

it "raises on invalid path" do
expect_raises(KeyError) do
data.dig("invalid.path")
end
end
end

describe "#dig?" do
it "returns Value(Nil) for invalid path" do
data.dig?("invalid.path").should be_a Metadata::Value(Nil)
end
end

describe "#[]" do
it "returns expected value" do
data.dig("foo").should eq Metadata::Value.new("bar")
end

it "raises on invalid key" do
expect_raises(KeyError) do
data.dig("invalid")
end
end
end

describe "#[]?" do
it "raises on invalid key" do
data["invalid"]?.should be_a Metadata::Value(Nil)
end
end
end
end
6 changes: 6 additions & 0 deletions src/lavinmq/amqp/channel.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require "./client"
require "./consumer"
# require "../public_info"
require "./stream_consumer"
require "../error"
require "../logger"
Expand All @@ -14,6 +15,7 @@ module LavinMQ
class Channel < LavinMQ::Client::Channel
include Stats
include SortableJSON
# include PublicInfo

getter id, name
property? running = true
Expand Down Expand Up @@ -73,6 +75,10 @@ module LavinMQ
}
end

# def public_info : PublicInfoData
# ChannelPublicInfoData.new details_tuple
# end

spuun marked this conversation as resolved.
Show resolved Hide resolved
def flow(active : Bool)
@flow = active
@consumers.each &.flow(active)
Expand Down
37 changes: 6 additions & 31 deletions src/lavinmq/http/controller.cr
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,12 @@ module LavinMQ
return iterator unless raw_name = params["name"]?
term = URI.decode_www_form(raw_name)
if params["use_regex"]?.try { |v| v == "true" }
iterator.select { |v| match_value(v).to_s =~ /#{term}/ }
iterator.select { |v| v[:name].to_s =~ /#{term}/ }
else
iterator.select { |v| match_value(v).to_s.includes?(term) }
iterator.select { |v| v[:name].to_s.includes?(term) }
end
end

protected def match_value(value)
value[:name]? || value["name"]?
end

MAX_PAGE_SIZE = 10_000

private def page(context, iterator : Iterator(SortableJSON))
Expand All @@ -44,26 +40,14 @@ module LavinMQ
return context
end
iterator = iterator.map do |i|
i.details_tuple
rescue e
{error: e.message}
i.metadata
spuun marked this conversation as resolved.
Show resolved Hide resolved
end
all_items = filter_values(params, iterator)
if sort_by = params.fetch("sort", nil).try &.split(".")

if sort_by = params.fetch("sort", nil)
sorted_items = all_items.to_a
filtered_count = sorted_items.size
if first_element = sorted_items.first?
{% begin %}
case dig(first_element, sort_by)
{% for k in {Int32, UInt16, UInt32, UInt64, Float64} %}
when {{k.id}}
sorted_items.sort_by! { |i| dig(i, sort_by).as({{k.id}}) }
{% end %}
else
sorted_items.sort_by! { |i| dig(i, sort_by).to_s.downcase }
end
{% end %}
end
sorted_items.sort_by! { |i| i.dig?(sort_by) }
sorted_items.reverse! if params["sort_reverse"]?.try { |s| !(s =~ /^false$/i) }
all_items = sorted_items.each
end
Expand Down Expand Up @@ -95,15 +79,6 @@ module LavinMQ
context
end

private def dig(i : NamedTuple, keys : Array(String))
if keys.size > 1
nt = i[keys.first].as?(NamedTuple) || return
dig(nt, keys[1..])
else
i[keys.first]? || 0
end
end

private def array_iterator_to_json(json, iterator, columns : Array(String)?, start : Int, page_size : Int)
size = 0
total = 0
Expand Down
113 changes: 113 additions & 0 deletions src/lavinmq/metadata.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
module AMQ::Protocol
struct Table
end
end

spuun marked this conversation as resolved.
Show resolved Hide resolved
module LavinMQ
# Wraps a generic NamedTuple to make it possible to add
# methods to it. Used in e.g. HTTP::Controller.
struct Metadata(T)
# This is similar to JSON::Any..
struct Value(T) # , V)
include Comparable(Value)

def self.nil
new(nil)
end

def initialize(@value : T)
@type = T
end

def type
T
end

def value : T
@value
end

def <=>(other : Value)
return 0 if @value.nil? && [email protected]?
return -1 if @value.nil?
return 1 if [email protected]?

if self.type != other.type
return @value.to_s <=> [email protected]_s
end
spuun marked this conversation as resolved.
Show resolved Hide resolved

if (value = @value) && (other_value = [email protected]?(T))
if value.is_a?(Comparable)
return value <=> other_value
end
end

return 0
end

delegate to_json, to_s, to: @value
end

def initialize(@data : T)
end

def self.empty
new NamedTuple.new
end

delegate to_json, to_s, to: @data

# Takes a dot separated path and returns the value at that path
# If T is `{a: {b: {c: 1} d: "foo"}` #dig("a.b.c") returns a Value(Int32)
# and #dig("a.d") returns a Value(String)

def dig(path : Symbol | String)
fetch(path) { raise KeyError.new "Invalid path: #{path.inspect}" }
end

def dig(path : Symbol | String)
fetch(path) { raise KeyError.new "Invalid path: #{path.inspect}" }
end

def dig?(path : Symbol | String)
fetch(path) { Value.nil }
end

def [](key : Symbol | String)
fetch(key) { raise KeyError.new "Missing key: #{key.inspect}" }
end

def []?(key : Symbol | String)
fetch(key) { Value.nil }
end

private def fetch(path : Symbol | String, &default : -> Value)
{% begin %}
{%
paths = [] of Array(String)
to_process = T.keys.map { |k| {[k], T[k]} }
# This will walk the "namedtuple tree" and find all paths to values
to_process.each do |(path, type)|
if type <= NamedTuple
paths << path
type.keys.each { |k| to_process << {path + [k], type[k]} }
else
paths << path
end
end
%}
case path
{% for path in paths %}
when {{path.join(".")}}, :{{path.join(".")}}
if value = @data[:"{{path.join("\"][:\"").id}}"]
return Value.new(value)
end
return Value.nil
{% end %}
else
yield
end
{% end %}
end
end
end
6 changes: 6 additions & 0 deletions src/lavinmq/sortable_json.cr
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
require "./metadata"

module LavinMQ
module SortableJSON
abstract def details_tuple

def metadata
Metadata.new details_tuple
end

def to_json(json : JSON::Builder)
details_tuple.to_json(json)
end
Expand Down
Loading