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

Support determining the proper serializer for arbitrarily nested types #273

Merged
merged 3 commits into from
Mar 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .typos.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
[default.extend-words]
# Remove this once https://github.com/crate-ci/typos/issues/316 is resolved
referer = "referer"
recruse = "recurse"
recruses = "recurses"

[files]
extend-exclude = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ private def assert_success(code : String, *, line : Int32 = __LINE__) : Nil
end

describe ATH do
describe "Console" do
describe "Console", tags: "compiler" do
it "errors if no name is provided" do
assert_error "Console command 'TestCommand' has an 'ACONA::AsCommand' annotation but is missing the commands's name. It was not provided as the first positional argument nor via the 'name' field.", <<-CR
require "../../spec_helper.cr"
Expand Down
3 changes: 1 addition & 2 deletions src/components/framework/spec/view_controller_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ struct ViewControllerTest < ATH::Spec::APITestCase
self.get("/view/json-array").body.should eq %([{"id":10,"name":"Bob"},{"id":20,"name":"Sally"}])
end

@[Pending]
def test_json_serializable_nested_array : Nil
self.get("/view//json-array-nested").body.should eq %([[{"id":10,"name":"Bob"}]])
self.get("/view/json-array-nested").body.should eq %([[{"id":10,"name":"Bob"}]])
end

def test_json_serializable_empty_array : Nil
Expand Down
59 changes: 36 additions & 23 deletions src/components/framework/src/view/view.cr
Original file line number Diff line number Diff line change
Expand Up @@ -134,30 +134,43 @@ class Athena::Framework::View(T)
self.response.headers.merge! headers
end

# Does type reduction logic to determine what serializer the data should use.
# `nil` for `ASR::Serializable`, otherwise `JSON::Serializable`.
# Recurses over all the types within `T` to determine what serializer the data should use.
protected def serializable_data : T?
{% if (T <= JSON::Serializable) && !(T <= ASR::Serializable) %}
# Single JSON::Serializable object
self.data
{% elsif (T <= NamedTuple) && (T.keys.any? do |k|
ntt = T[k]

((ntt <= JSON::Serializable) && !(ntt <= ASR::Serializable)) ||
(ntt.union? && ntt.union_types.any? { |ut| ((ut <= JSON::Serializable) && !(ut <= ASR::Serializable)) }) ||
(ntt.type_vars.any? { |t| ((t <= JSON::Serializable) && !(t <= ASR::Serializable)) })
end) %}
# Namedtuple with a value of JSON::Serializable
self.data
{% elsif (T <= Enumerable) && T.type_vars.any? do |t|
((t <= JSON::Serializable) && !(t <= ASR::Serializable)) ||
(t.union? && t.union_types.any? { |ut| ((ut <= JSON::Serializable) && !(ut <= ASR::Serializable)) ||
(ut.type_vars.any? { |utt| ((utt <= JSON::Serializable) && !(utt <= ASR::Serializable)) }) })
end %}
# Enumerable (Hash, Array, Set, ...) of JSON::Serializable
self.data
{% else %}
nil
{% begin %}
{%
types_to_recurse = [T]

types_to_recurse.each do |t|
t = t.resolve

if t.union?
t.union_types.each do |ut|
types_to_recurse << ut
end
elsif t <= NamedTuple
t.keys.each do |k|
types_to_recurse << t[k].resolve
end
elsif t <= Enumerable
t.type_vars.each do |ut|
types_to_recurse << ut
end

# Use `to_json` if a type includes `JSON::Serializable` but not `ASR::Serializable`
elsif (t <= JSON::Serializable) && !(t <= ASR::Serializable)
use_serializer_component = false

# Use the serializer component if the type is `ASR::Serializable`
elsif (t <= ASR::Serializable)
use_serializer_component = true
else
# Fallback on the serializer component
use_serializer_component = true
end
end
%}

{{ use_serializer_component == true ? nil : "self.data".id }}
{% end %}
end
end