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

Optional and Object for InterTypes #96

Merged
merged 6 commits into from
Jan 23, 2024
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
1 change: 1 addition & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Reexport = "189a3867-3050-52da-a836-e630ba90ab69"
SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce"
StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
StructEquality = "6ec83bb0-ed9f-11e9-3b4c-2b04cb4e219c"
StructTypes = "856f2bd8-1eba-4b0a-8007-ebc267875bd4"
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"

[weakdeps]
Expand Down
35 changes: 34 additions & 1 deletion src/intertypes/InterTypes.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module InterTypes
export InterType, InterTypeDecl, Binary, LanguageTarget, SerializationTarget, generate_module, intertype, @intertypes
export InterType, InterTypeDecl, Object, Optional,
LanguageTarget, SerializationTarget, generate_module, intertype, @intertypes

using MLStyle
using OrderedCollections
Expand Down Expand Up @@ -56,6 +57,8 @@ end
Binary
List(elemtype::InterType)
Map(keytype::InterType, valuetype::InterType)
ObjectType(elemtype::InterType)
OptionalType(elemtype::InterType)
Record(fields::Vector{Field{InterType}})
Sum(variants::Vector{Variant{InterType}})
ACSetInterType(schema::TypedSchema{InterType})
Expand Down Expand Up @@ -132,6 +135,36 @@ function generate_module(mod::Module, target::Type{<:ExportTarget}, path="."; ta
generate_module(mod.Meta, target, path; target_specific_args...)
end

module InterTypeSupport
using OrderedCollections
using StructEquality
import StructTypes
export Object, Optional

@struct_hash_equal struct Object{T}
fields::OrderedDict{Symbol, T}
jpfairbanks marked this conversation as resolved.
Show resolved Hide resolved
end

function Object{T}(pairs::(Pair{Symbol, S} where {S<:T})...) where {T}
Object{T}(OrderedDict{Symbol, T}(pairs...))
end

function Object(pairs::(Pair{Symbol, T} where {T})...)
Object{Any}(pairs...)
end

Base.getindex(obj::Object, key::Symbol) = obj.fields[key]
Base.setindex!(obj::Object, x, key::Symbol) = (obj.fields[key] = x)

Base.pairs(obj::Object) = pairs(obj.fields)

StructTypes.StructType(::Type{<:Object}) = StructTypes.DictType()

const Optional{T} = Union{T, Nothing}
end

using .InterTypeSupport

include("json.jl")
include("sexp.jl")
include("julia.jl")
Expand Down
135 changes: 81 additions & 54 deletions src/intertypes/json.jl
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,27 @@
print(io, "]")
end

intertype(::Type{Object{T}}) where {T} = ObjectType{intertype(T)}

Check warning on line 99 in src/intertypes/json.jl

View check run for this annotation

Codecov / codecov/patch

src/intertypes/json.jl#L99

Added line #L99 was not covered by tests
function read(format::JSONFormat, ::Type{Object{T}}, s::JSON3.Object) where {T}
Object{T}(
[k => read(format, T, v) for (k, v) in pairs(s)]...
)
end
function write(io::IO, format::JSONFormat, d::Object)
writeobject(io) do next
for p in pairs(d)
next()
writekv(io, p)
end
end
end

intertype(::Type{Optional{T}}) where {T} = Optional{intertype(T)}

Check warning on line 114 in src/intertypes/json.jl

View check run for this annotation

Codecov / codecov/patch

src/intertypes/json.jl#L114

Added line #L114 was not covered by tests
read(::JSONFormat, ::Type{Optional{T}}, ::Nothing) where {T} = nothing
read(format::JSONFormat, ::Type{Optional{T}}, s) where {T} =
read(format, T, s)
write(io::IO, format::JSONFormat, d::Nothing) = print(io, "null")

intertype(::Type{OrderedDict{K,V}}) where {K,V} = Map(intertype(K), intertype(V))
function read(format::JSONFormat, ::Type{OrderedDict{K, V}}, s::JSON3.Array) where {K, V}
res = OrderedDict{K, V}()
Expand Down Expand Up @@ -242,68 +263,75 @@
end
end

const Object = OrderedDict{String, Any}

function fieldproperties(fields::Vector{Field{InterType}})
map(fields) do field
string(field.name) => tojsonschema(field.type)
field.name => tojsonschema(field.type)
end
end

function tojsonschema(type::InterType)
@match type begin
I32 => Object(
"type" => "integer",
"\$comment" => "I32",
"minimum" => typemin(Int32),
"maximum" => typemax(Int32)
:type => "integer",
Symbol("\$comment") => "I32",
:minimum => typemin(Int32),
:maximum => typemax(Int32)
)
U32 => Object(
"type" => "integer",
"\$comment" => "U32",
"minimum" => typemin(UInt32),
"maximum" => typemax(UInt32)
:type => "integer",
Symbol("\$comment") => "U32",
:minimum => typemin(UInt32),
:maximum => typemax(UInt32)
)
I64 => Object(
"type" => "string",
"\$comment" => "I64"
:type => "string",
Symbol("\$comment") => "I64"
)
U64 => Object(
"type" => "string",
"\$comment" => "U64"
:type => "string",
Symbol("\$comment") => "U64"
)
F64 => Object(
"type" => "number",
"\$comment" => "F64"
:type => "number",
Symbol("\$comment") => "F64"
)
Boolean => Object(
"type" => "boolean",
"\$comment" => "Boolean"
:type => "boolean",
Symbol("\$comment") => "Boolean"
)
Str => Object(
"type" => "string",
"\$comment" => "Str"
:type => "string",
Symbol("\$comment") => "Str"
)
Sym => Object(
"type" => "string",
"\$comment" => "Sym"
:type => "string",
Symbol("\$comment") => "Sym"
)
Binary => Object(
"type" => "string",
"contentEncoding" => "base64",
"\$comment" => "Binary"
:type => "string",
:contentEncoding => "base64",
Symbol("\$comment") => "Binary"
)
OptionalType(elemtype) => begin
schema = tojsonschema(elemtype)
schema[:type] = [schema[:type], "null"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The schema of a type can be a list including the string null? Is this a JSONSchema idiom?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so? This is what showed up on the jsonschema website when I poked around for it, but I'm no jsonschema expert.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fivegrant can probably confirm that it is what he would expect for a nullable field.

schema
end
ObjectType(elemtype) => Object(
:type => "object",
:additionalProperties => tojsonschema(elemtype)
)
List(elemtype) => Object(
"type" => "array",
"items" => tojsonschema(elemtype)
:type => "array",
:items => tojsonschema(elemtype)
)
Map(keytype, valuetype) => Object(
"type" => "array",
"items" => Object(
"type" => "object",
"properties" => Object(
"key" => tojsonschema(keytype),
"value" => tojsonschema(valuetype)
:type => "array",
:items => Object(
:type => "object",
:properties => Object(
:key => tojsonschema(keytype),
:value => tojsonschema(valuetype)
)
)
)
Expand All @@ -321,24 +349,24 @@
end

reftype(name) = Object(
"\$ref" => "#/\$defs/$(name)"
Symbol("\$ref") => "#/\$defs/$(name)"
)

recordtype(fields) = Object(
"type" => "object",
"properties" => Object(fieldproperties(fields)),
"required" => string.(nameof.(fields))
:type => "object",
:properties => Object(fieldproperties(fields)...),
:required => string.(nameof.(fields))
)

varianttype(variant) = Object(
"type" => "object",
"properties" => Object(
"tag" => Object(
"const" => string(variant.tag)
:type => "object",
:properties => Object(
:tag => Object(
:const => string(variant.tag)
),
fieldproperties(variant.fields)...
),
"required" => string.(nameof.(variant.fields))
:required => string.(nameof.(variant.fields))
)

function acsettype(spec)
Expand All @@ -353,8 +381,8 @@
Field{InterType}(ob, List(Record([idfield; homfields; attrfields])))
end
Object(
"type" => "object",
"properties" => recordtype(tablespecs)
:type => "object",
:properties => recordtype(tablespecs)
)
end

Expand All @@ -370,27 +398,26 @@
mod::InterTypeModule, ::Type{JSONTarget}, path
;ac=JSON3.AlignmentContext(indent=2)
)
defs = Pair{String, Object}[]
defs = Pair{Symbol, Object}[]
for (name, decl) in mod.declarations
sname = string(name)
@match decl begin
Alias(type) => push!(defs, sname => tojsonschema(type))
Struct(fields) => push!(defs, sname => recordtype(fields))
Alias(type) => push!(defs, name => tojsonschema(type))
Struct(fields) => push!(defs, name => recordtype(fields))
VariantOf(parent) => begin
sum = mod.declarations[parent]
variant = only(filter(v -> v.tag == name, sum.variants))
push!(defs, sname => varianttype(variant))
push!(defs, name => varianttype(variant))
end
SumType(variants) =>
push!(defs, sname => Object("oneOf" => reftype.([v.tag for v in variants])))
push!(defs, name => Object(:oneOf => reftype.([v.tag for v in variants])))
NamedACSetType(spec) =>
push!(defs, sname => acsettype(spec))
push!(defs, name => acsettype(spec))
_ => nothing
end
end
schema = Object(
"\$schema" => "http://json-schema.org/draft-07/schema#",
"\$defs" => Object(defs)
Symbol("\$schema") => "http://json-schema.org/draft-07/schema#",
Symbol("\$defs") => Object(defs...)
)
schema_filepath = joinpath(path, string(mod.name)*"_schema.json")
open(schema_filepath, "w") do io
Expand Down
7 changes: 7 additions & 0 deletions src/intertypes/julia.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using ...ACSets


function parse_fields(fieldexprs; mod)
map(enumerate(fieldexprs)) do (i, fieldexpr)
@match fieldexpr begin
Expand Down Expand Up @@ -111,6 +112,10 @@ function parse_intertype(e; mod::InterTypeModule)
InterTypes.List(parse_intertype(elemtype; mod))
Expr(:curly, :OrderedDict, keytype, valuetype) =>
InterTypes.Map(parse_intertype(keytype; mod), parse_intertype(valuetype; mod))
Expr(:curly, :Object, elemtype) =>
InterTypes.ObjectType(parse_intertype(elemtype; mod))
Expr(:curly, :Optional, elemtype) =>
InterTypes.OptionalType(parse_intertype(elemtype; mod))
Expr(:curly, :Record, fieldexprs...) => begin
InterTypes.Record(parse_fields(fieldexprs; mod))
end
Expand Down Expand Up @@ -200,6 +205,8 @@ function toexpr(intertype::InterType)
Binary => :(Vector{UInt8})
List(elemtype) => Expr(:curly, :Vector, toexpr(elemtype))
Map(keytype, valuetype) => Expr(:curly, :OrderedDict, toexpr(keytype), toexpr(valuetype))
ObjectType(elemtype) => Expr(:curly, InterTypeSupport.Object, toexpr(elemtype))
OptionalType(elemtype) => Expr(:curly, InterTypeSupport.Optional, toexpr(elemtype))
Record(fields) =>
Expr(:curly, :Record, toexpr.(fields)...)
Sum(variants) =>
Expand Down
2 changes: 2 additions & 0 deletions src/intertypes/python.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ function topy(intertype::InterType; forward_ref=true)
Str => "str"
Sym => "str"
Binary => "str"
OptionalType(elemtype) => "$(topy(elemtype)) | None"
ObjectType(elemtype) => "dict[str, $(topy(elemtype))]"
List(elemtype) => "list[$(topy(elemtype))]"
Map(keytype, valuetype) => "OrderedDict[$(topy(keytype)), $(topy(valuetype))]"
Record(_) => error("no native record type for python")
Expand Down
4 changes: 4 additions & 0 deletions src/intertypes/sexp.jl
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ function tosexp(t::InterType)
SExp(:List, tosexp(elemtype))
Map(keytype, valuetype) =>
SExp(:Map, tosexp(keytype), tosexp(valuetype))
ObjectType(elemtype) =>
SExp(:Object, tosexp(elemtype))
OptionalType(elemtype) =>
SExp(:Optional, tosexp(elemtype))
Record(fields) => SExp(:Record, fieldsexps(fields)...)
Sum(variants) => SExp(:Sum, variantsexps(variants)...)
ACSetInterType(schema) => :ACset
Expand Down
Loading