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

Add @auto macro to automatically provide StructTypes definitions for a type tree #97

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
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
72 changes: 68 additions & 4 deletions src/macros.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"""
@$(isolate_name($struct_type))(expr::Expr)
@$(isolate_name($struct_type))(expr::Symbol)

If `expr` is a struct definition, sets the `StructType` of the defined struct to
`$(isolate_name($struct_type))()`. If `expr` is the name of a `Type`, sets the `StructType` of that
type to `$(isolate_name($struct_type))()`.
Expand All @@ -18,7 +18,7 @@
```julia
StructTypes.StructType(::Type{MyStruct}) = StructType.Struct()
```
and
and
```julia
@$(isolate_name($struct_type)) struct MyStruct
val::Int
Expand Down Expand Up @@ -59,7 +59,7 @@

"""
Macro to add subtypes for an abstract type without the need for type field.
For a given `abstract_type`` and `struct_subtype` generates custom lowered NameTuple
For a given `abstract_type`` and `struct_subtype` generates custom lowered NameTuple
with all subtype fields and additional `StructTypes.subtypekey` field
used for identifying the appropriate concrete subtype.

Expand Down Expand Up @@ -87,4 +87,68 @@
StructTypes.lowertype(::Type{$(esc(struct_subtype))}) = @NamedTuple{$field_name::$(esc(Symbol)), $(name_types...)}
$(esc(struct_subtype))(x::@NamedTuple{$field_name::$(esc(Symbol)), $(name_types...)}) = $(esc(struct_subtype))($(x_names...))
end
end
end


#-----------------------------------------------------------------------------# @auto


"""
@auto AbstractSuperType
@auto AbstractSuperType _my_subtype_key_

Macro to automatically generate the StructTypes interface for every subtype of `AbstractSuperType`.
With the example of JSON3, this enables you to `JSON3.read(str, MyType)` where `MyType <: AbstractSuperType`.

`@auto` assumes that all subtypes of the provided `AbstractSuperType` have the default constructors
provided by Julia. If this is not the case, you'll need to overload:

StructTypes.construct(::Type{MyType}, named_tuple::StructTypes.lowertype(MyType)) = MyType(...)

"""
macro auto(T, subtypekey = :__type__)
esc(quote

if $T isa UnionAll
@warn "Cannot use @auto with UnionAll types."
else
function StructTypes.StructType(::Type{T}) where {T <: $T}
isconcretetype(T) ? StructTypes.CustomStruct() : StructTypes.AbstractType()
end

function StructTypes.lower(x::T) where {T <: $T}
($subtypekey = Symbol(T), NamedTuple(k => getfield(x, k) for k in fieldnames(T))...)
end

function StructTypes.lowertype(::Type{T}) where {T <: $T}
NamedTuple{($(QuoteNode(subtypekey)), fieldnames(T)...), Tuple{Symbol, fieldtypes(T)...}}

Check warning on line 124 in src/macros.jl

View check run for this annotation

Codecov / codecov/patch

src/macros.jl#L123-L124

Added lines #L123 - L124 were not covered by tests
end

function StructTypes.construct(::Type{T}, nt::StructTypes.lowertype(Type{T})) where T <: $T
T((nt[x] for x in fieldnames(T))...)
end

function StructTypes.subtypes(::Type{T}) where {T <: $T}
StructTypes.SubTypeClosure() do x::Symbol
e = Meta.parse(string(x))
if StructTypes.is_valid_type_ex(e)
try # try needed to catch undefined symbols
S = eval(e)
S isa Type && S <: T && return S
catch

Check warning on line 138 in src/macros.jl

View check run for this annotation

Codecov / codecov/patch

src/macros.jl#L131-L138

Added lines #L131 - L138 were not covered by tests
end
end
return Any

Check warning on line 141 in src/macros.jl

View check run for this annotation

Codecov / codecov/patch

src/macros.jl#L141

Added line #L141 was not covered by tests
end
end

StructTypes.subtypekey(T::Type{<: $T}) = $(QuoteNode(subtypekey))

Check warning on line 145 in src/macros.jl

View check run for this annotation

Codecov / codecov/patch

src/macros.jl#L145

Added line #L145 was not covered by tests
end
end)
end

is_valid_type_ex(x) = isbits(x)

Check warning on line 150 in src/macros.jl

View check run for this annotation

Codecov / codecov/patch

src/macros.jl#L150

Added line #L150 was not covered by tests

is_valid_type_ex(s::Union{Symbol, QuoteNode}) = true

Check warning on line 152 in src/macros.jl

View check run for this annotation

Codecov / codecov/patch

src/macros.jl#L152

Added line #L152 was not covered by tests

is_valid_type_ex(e::Expr) = ((e.head == :curly || e.head == :tuple || e.head == :.) && all(map(is_valid_type_ex, e.args)))

Check warning on line 154 in src/macros.jl

View check run for this annotation

Codecov / codecov/patch

src/macros.jl#L154

Added line #L154 was not covered by tests
44 changes: 40 additions & 4 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ end
builtin_type_mapping = Dict(
StructTypes.AbstractType() => Union{
Core.IO,
Core.Number,
Core.Number,
Base.AbstractDisplay,
Base.VERSION <= v"1.2" ? Union{} : Union{
Base.AbstractMatch,
Base.AbstractPattern,
}
}
},
StructTypes.UnorderedStruct() => Union{
Core.Any, # Might be too open
Expand Down Expand Up @@ -676,7 +676,7 @@ function bicycle_subtypes(t_sym::Symbol)
isempty(t[1]) && t[2] == 1 && return Gravel # gravel bikes often have 1 x N chainring/cog setups
end
sub_type_closure = StructTypes.SubTypeClosure(bicycle_subtypes)
StructTypes.subtypes(::Type{Bicycle}) = sub_type_closure
StructTypes.subtypes(::Type{Bicycle}) = sub_type_closure

mutable struct C2
a::Int
Expand Down Expand Up @@ -855,4 +855,40 @@ StructTypes.@register_struct_subtype Vehicle2 Truck2
@test StructTypes.lowertype(Car2) === typeof(nt)
@test typeof(car) == Car2
@test car.make == "Mercedes-Benz"
end
end


# @auto
abstract type AbstractSuperType end
StructTypes.@auto AbstractSuperType

abstract type AbstractSubType <: AbstractSuperType end
struct AutoA <: AbstractSuperType
x::Int
y::String
z::Symbol
end
struct AutoB <: AbstractSubType
x::String
y::Symbol
z::Float64
end
struct AutoC{T, S} <: AbstractSubType
x::T
y::S
z::String
end


@testset "@auto" begin
@test StructTypes.StructType(AutoC) == StructTypes.AbstractType()
@test StructTypes.StructType(AutoC{Int,Int}) == StructTypes.CustomStruct()

a = AutoA(1, "2", :three)
b = AutoB("one", :two, 3.0)
c = AutoC(:one, "two", "three")

@test StructTypes.construct(AutoA, StructTypes.lower(a)) == a
@test StructTypes.construct(AutoB, StructTypes.lower(b)) == b
@test StructTypes.construct(AutoC{Symbol,String}, StructTypes.lower(c)) == c
end
Loading