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
87 changes: 87 additions & 0 deletions spec/metadata_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
require "./spec_helper"

module LavinMQ
struct NamedTupleMetadata(T) < Metadata
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},
{1u8, 2i32, -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
39 changes: 6 additions & 33 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 &.[:name].to_s.matches?(/#{term}/)
else
iterator.select { |v| match_value(v).to_s.includes?(term) }
iterator.select &.[: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 @@ -43,27 +39,13 @@ module LavinMQ
{error: "payload_too_large", reason: "Max allowed page_size 10000"}.to_json(context.response)
return context
end
iterator = iterator.map do |i|
i.details_tuple
rescue e
{error: e.message}
end
iterator = iterator.map &.metadata
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! &.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 +77,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
117 changes: 117 additions & 0 deletions src/lavinmq/metadata.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
module LavinMQ
abstract struct Metadata
def self.new(data : NamedTuple)
NamedTupleMetadata.new(data)
end

# This is similar to JSON::Any..
struct Value(T) # , V)
include Comparable(Value)

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

def self.nil
new(nil)
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 T <= Number %}
if other_value = [email protected]?(Number)
return @value <=> other_value
end
{% end %}

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

{% if T <= Comparable %}
if (value = @value) && (other_value = [email protected]?(T))
return value <=> other_value
end
{% end %}

0
end

delegate to_json, to_s, to: @value
end
end

# Wraps a generic NamedTuple to make it possible to add
# methods to it. Used in e.g. HTTP::Controller.
struct NamedTupleMetadata(T) < Metadata
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) { 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)
# This will walk the "namedtuple tree" and find all paths to values. It's not
# possible to do recursive macros, but ArrayLiteral#each will iterate over items
# added while currently iterating.
to_visit = T.keys.map { |k| {[k], T[k]} }
to_visit.each do |(path, type)|
if type <= NamedTuple
paths << path
type.keys.each { |k| to_visit << {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