Skip to content

Commit

Permalink
attempt to add serializable annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
danini-the-panini committed Feb 20, 2025
1 parent 3eea07d commit 0991900
Show file tree
Hide file tree
Showing 6 changed files with 295 additions and 3 deletions.
95 changes: 95 additions & 0 deletions spec/kdl/serializable_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
require "../spec_helper"

class TestChild
include KDL::Serializable

@[KDL::Argument]
property value : String

def initialize(@value)
end
end

class TestNode
include KDL::Serializable

@[KDL::Argument]
property first : String

@[KDL::Argument]
property second : Bool

@[KDL::Arguments]
property numbers : Array(UInt32)

@[KDL::Property]
property foo : String

@[KDL::Property(name: "bardle")]
property bar : String

@[KDL::Properties]
property map : Hash(String, String)

@[KDL::Child(unwrap: "argument")]
property arg : String

@[KDL::Child(unwrap: "arguments")]
property args : Array(String)

@[KDL::Child(unwrap: "properties")]
property props : Hash(String, String)

@[KDL::Child]
property norf : TestChild

@[KDL::Children(name: "thing")]
property things : Array(TestChild)

def initialize(@first, @second, @numbers, @foo, @bar, @map, @arg, @args, @props, @norf, @things)
end
end

class TestDocument
include KDL::Serializable

@[KDL::Child]
property node : TestNode

def initialize(@node)
end
end

describe KDL::Serializable do
it "serializes documents" do
doc = KDL.parse <<-KDL
node "arg1" #true 1 22 33 foo="a" bardle="b" baz="c" qux="d" {
arg "arg2"
args "x" "y" "z"
props a="x" b="y" c="z"
norf "wat"
thing "foo"
thing "bar"
thing "baz"
}
KDL

obj = TestDocument.from_kdl(doc)
obj.node.first.should eq "arg1"
obj.node.second.should eq true
obj.node.numbers.should eq [1, 22, 333]
obj.node.foo.should eq "a"
obj.node.bar.should eq "b"
obj.node.map.should eq({ "baz": "c", "qux": "d" })
obj.node.arg.should eq "arg2"
obj.node.args.should eq ["x", "y", "z"]
obj.node.props.should eq({ "a": "x", "b": "y", "c": "z" })
obj.node.norf.value.should eq "wat"
obj.node.things.size.should eq 3
obj.node.things[0].value.should eq "foo"
obj.node.things[1].value.should eq "bar"
obj.node.things[2].value.should eq "baz"

obj.to_kdl.should eq doc
end
end
2 changes: 1 addition & 1 deletion src/kdl/builder.cr
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ module KDL
if parent = current_node
parent.children << node
else
document.nodes << node
document << node
end
end

Expand Down
14 changes: 14 additions & 0 deletions src/kdl/document.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,26 @@ require "./node"

module KDL
class Document
include Enumerable(Node)

property nodes
property comment

def initialize(@nodes = [] of Node, *, @comment : String? = nil)
end

def each(&)
@nodes.each { |n| yield n }
end

def <<(node)
@nodes << node
end

def empty?
@nodes.empty?
end

def [](index : Int)
nodes[index]
end
Expand Down
2 changes: 1 addition & 1 deletion src/kdl/node.cr
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ module KDL
*,
@arguments = [] of KDL::Value,
@properties = {} of String => KDL::Value,
@children = [] of Node,
@children : KDL::Document = KDL::Document.new,
@type : String? = nil,
@comment : String? = nil,
)
Expand Down
2 changes: 1 addition & 1 deletion src/kdl/parser.cr
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ module KDL
@depth += 1
children = parse_children
@depth -= 1
node.children = children unless commented
node.children = KDL::Document.new(children) unless commented
end

private def parse_rbrace
Expand Down
183 changes: 183 additions & 0 deletions src/kdl/serializable.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
module KDL
annotation Argument; end
annotation Arguments; end
annotation Property; end
annotation Properties; end
annotation Child; end
annotation Children; end

module Serializable
macro included
def self.new(node : ::KDL::Node)
new_from_kdl_node(node)
end

def self.new(doc : ::KDL::Document)
new_from_kdl_node(::KDL::Node.new("__root", children: doc))
end

private def self.new_from_kdl_node(node : ::KDL::Node)
instance = allocate
instance.initialize(__node_for_kdl_serializable: node)
::GC.add_finalizer(instance) if instance.responds_to?(:finalize)
instance
end

macro inherited
def self.new(doc : ::KDL::Document)
new_from_kdl_document(doc)
end
end
end

def initialize(*, __node_for_kdl_serializable node : ::KDL::Node)
{% begin %}
{% argument_annos = [] of Nil %}
{% arguments_anno = nil %}
{% property_annos = {} of Nil => Nil %}
{% properties_anno = nil %}
{% child_annos = {} of Nil => Nil %}
{% children_annos = {} of Nil => Nil %}
{% other_properties = {} of Nil => Nil %}

{% for ivar in @type.instance_vars %}
{% if ann = ivar.annotation(::KDL::Argument) %}
{% unless ann[:ignore] || ann[:ignore_deserialize] %}
{%
argument_annos << {
id: ivar.id,
has_default: ivar.has_default_value?,
default: ivar.default_value,
nilable: ivar.type.nilable?,
type: ivar.type,
converter: ann[:converter],
presence: ann[:presence]
}
%}
{% end %}
{% elsif ann = ivar.annotation(::KDL::Arguments) %}
{% unless ann[:ignore] || ann[:ignore_deserialize] %}
{%
arguments_anno = {
id: ivar.id,
has_default: ivar.has_default_value?,
default: ivar.default_value,
nilable: ivar.type.nilable?,
type: ivar.type,
converter: ann[:converter],
presence: ann[:presence]
}
%}
{% end %}
{% elsif ann = ivar.annotation(::KDL::Property) %}
{% unless ann[:ignore] || ann[:ignore_deserialize] %}
{%
property_annos[ivar.id] = {
key: (ann[:key] || ivar).id.stringify,
has_default: ivar.has_default_value?,
default: ivar.default_value,
nilable: ivar.type.nilable?,
type: ivar.type,
converter: ann[:converter],
presence: ann[:presence]
}
%}
{% end %}
{% elsif ann = ivar.annotation(::KDL::Properties) %}
{% unless ann[:ignore] || ann[:ignore_deserialize] %}
{%
properties_anno = {
id: ivar.id,
has_default: ivar.has_default_value?,
default: ivar.default_value,
nilable: ivar.type.nilable?,
type: ivar.type,
converter: ann[:converter],
presence: ann[:presence]
}
%}
{% end %}
{% elsif ann = ivar.annotation(::KDL::Child) %}
{% unless ann[:ignore] || ann[:ignore_deserialize] %}
{%
child_annos[ivar.id] = {
key: (ann[:key] || ivar).id.stringify,
has_default: ivar.has_default_value?,
default: ivar.default_value,
nilable: ivar.type.nilable?,
type: ivar.type,
converter: ann[:converter],
presence: ann[:presence],
unwrap: ann[:unwrap]
}
%}
{% end %}
{% elsif ann = ivar.annotation(::KDL::Children) %}
{% unless ann[:ignore] || ann[:ignore_deserialize] %}
{%
children_annos[ivar.id] = {
key: (ann[:key] || ivar).id.stringify,
has_default: ivar.has_default_value?,
default: ivar.default_value,
nilable: ivar.type.nilable?,
type: ivar.type,
converter: ann[:converter],
presence: ann[:presence]
}
%}
{% end %}
{% else %}
{%
other_children[ivar.id] = {
key: ivar.id.stringify,
has_default: ivar.has_default_value?,
default: ivar.default_value,
nilable: ivar.type.nilable?,
type: ivar.type
}
%}
{% end %}
{% end %}

{% for value, index in argument_annos %}
%var{value[:id]} = node[{{index}}]
%found{value[:id]} = true
{% end %}
{% if arguments_anno %}
%var{arguments_anno[:id]} = node.arguments[{{argument_annos.size}}..].map(&.value)
%found{arguments_anno[:id]} = true
{% end %}
found_props = [] of String
{% for name, value in property_annos %}
%var{name} = node[{{name.stringify}}]
%found{name} = true
found_props << {{name.stringify}}
{% end %}
{% if properties_anno %}
%var{properties_anno[:id]} = node.properties.reject(found_props)
%found{properties_anno[:id]} = true
{% end %}
{% for name, value in child_annos %}
{% if value[:unwrap] == "argument" %}
%var{name} = node.arg({{name.stringify}})
{% elsif value[:unwrap] == "arguments" %}
%var{name} = node.args({{name.stringify}})
{% elsif value[:unwrap] == "properties" %}
%var{name} = node.child({{name.stringify}}).properties.transform_values { |v, _| v.value }
{% else %}
%var{name} = {{value[:type]}}.from_kdl(node.child({{name.stringify}}))
{% end %}
%found{name} = true
{% end %}
{% for name, value in children_annos %}
%var{name} = node.children.select { |n| n.name == {{name.stringify}} }.map { |n| {{value[:type]}}.from_kdl(n) }
%found{name} = true
{% end %}
{% end %}
end
end
end

def Object.from_kdl(doc)
new doc
end

0 comments on commit 0991900

Please sign in to comment.