From 5d5d7716367eac4eba6a83917cf1cfccf4e6c491 Mon Sep 17 00:00:00 2001 From: Jacob Quinn Date: Wed, 22 May 2024 22:36:22 -0600 Subject: [PATCH] Rename from Structs -> StructUtils --- LICENSE.md | 2 +- Project.toml | 2 +- README.md | 34 ++++---- src/Structs.jl | 132 ++++++++++++++--------------- src/macros.jl | 30 +++---- src/selectors.jl | 214 +++++++++++++++++++++++++++++++++++++++++++++++ test/macros.jl | 46 +++++----- test/runtests.jl | 212 +++++++++++++++++++++++----------------------- test/struct.jl | 14 ++-- 9 files changed, 449 insertions(+), 237 deletions(-) create mode 100644 src/selectors.jl diff --git a/LICENSE.md b/LICENSE.md index 53c233f..1a83489 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Structs.jl is licensed under the MIT License: +StructUtils.jl is licensed under the MIT License: Copyright (c) 2024: Jacob Quinn diff --git a/Project.toml b/Project.toml index 7dabf01..5d0289d 100644 --- a/Project.toml +++ b/Project.toml @@ -1,4 +1,4 @@ -name = "Structs" +name = "StructUtils" uuid = "ec057cc2-7a8d-4b58-b3b3-92acb9f63b42" version = "1.0.0" diff --git a/README.md b/README.md index 17b570c..3249cf3 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,18 @@ -# Structs +# StructUtils -[![CI](https://github.com/quinnj/Structs.jl/workflows/CI/badge.svg)](https://github.com/quinnj/Structs.jl/actions?query=workflow%3ACI) -[![codecov](https://codecov.io/gh/quinnj/Structs.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/quinnj/Structs.jl) -[![deps](https://juliahub.com/docs/Structs/deps.svg)](https://juliahub.com/ui/Packages/Structs/HHBkp?t=2) -[![version](https://juliahub.com/docs/Structs/version.svg)](https://juliahub.com/ui/Packages/Structs/HHBkp) -[![pkgeval](https://juliahub.com/docs/Structs/pkgeval.svg)](https://juliahub.com/ui/Packages/Structs/HHBkp) - -\*\* +[![CI](https://github.com/quinnj/StructUtils.jl/workflows/CI/badge.svg)](https://github.com/quinnj/StructUtils.jl/actions?query=workflow%3ACI) +[![codecov](https://codecov.io/gh/quinnj/StructUtils.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/quinnj/StructUtils.jl) +[![deps](https://juliahub.com/docs/StructUtils/deps.svg)](https://juliahub.com/ui/Packages/StructUtils/HHBkp?t=2) +[![version](https://juliahub.com/docs/StructUtils/version.svg)](https://juliahub.com/ui/Packages/StructUtils/HHBkp) +[![pkgeval](https://juliahub.com/docs/StructUtils/pkgeval.svg)](https://juliahub.com/ui/Packages/StructUtils/HHBkp) ## Installation -The package is registered in the [`General`](https://github.com/JuliaRegistries/General) registry and so can be installed at the REPL with `] add Structs`. +The package is registered in the [`General`](https://github.com/JuliaRegistries/General) registry and so can be installed at the REPL with `] add StructUtils`. ## Documentation -The primary interface provided by Structs.jl is in the form of the exported `@noarg`, `@defaults`, and `@tags` macros, along with the unexported (to avoid clashing with the Base definition) of `Structs.@kwdef`. These macros can be used on struct definitions to provide a more ergonomic and flexible way to define structs with default values, keyword constructors, and more. +The primary interface provided by StructUtils.jl is in the form of the exported `@noarg`, `@defaults`, and `@tags` macros, along with the unexported (to avoid clashing with the Base definition) of `StructUtils.@kwdef`. These macros can be used on struct definitions to provide a more ergonomic and flexible way to define structs with default values, keyword constructors, and more. The `@noarg` macro can be used to define a "no argument" constructor, and must be used with mutable structs. This allows for programmatic construction and discovery of the supported behavior. Default values and field tags can also be defined in `@noarg` structs. @@ -25,18 +23,18 @@ The `@tags` macro can be used to define tags for fields in any kind of struct. The `@kwdef` macro mirros the functionality of the Base defintion, while also allowing for the inclusion of field tags. -The other major interface Structs.jl provides is the `Structs.make(T, source)` function. It allows programmatic construction of a type `T` from a variety of source objects. -For example, I could have a custom struct `Foo` and be able to construct an instance from an array of values, a dictionary, a database cursor, a JSON object, etc. This is done by allowing source objects to implement interfaces for how fields should be provided programmatically (the primary means being the `Structs.applyeach` function), while `Structs.make` uses the programmatic knowledge from the above-mentioned macros, along with potential field tags, to construct the object. +The other major interface StructUtils.jl provides is the `StructUtils.make(T, source)` function. It allows programmatic construction of a type `T` from a variety of source objects. +For example, I could have a custom struct `Foo` and be able to construct an instance from an array of values, a dictionary, a database cursor, a JSON object, etc. This is done by allowing source objects to implement interfaces for how fields should be provided programmatically (the primary means being the `StructUtils.applyeach` function), while `StructUtils.make` uses the programmatic knowledge from the above-mentioned macros, along with potential field tags, to construct the object. -Additional documentation is forth-coming around how package developers can use the "under the hood" machinery of Structs.jl to provide a more flexible and ergonomic interface to their users, like custom serialization/deserialization, database interaction, etc. +Additional documentation is forth-coming around how package developers can use the "under the hood" machinery of StructUtils.jl to provide a more flexible and ergonomic interface to their users, like custom serialization/deserialization, database interaction, etc. ## Contributing and Questions Contributions are very welcome, as are feature requests and suggestions. Please open an [issue][issues-url] if you encounter any problems or would just like to ask a question. -[ci-img]: https://github.com/quinnj/Structs.jl/workflows/CI/badge.svg -[ci-url]: https://github.com/quinnj/Structs.jl/actions?query=workflow%3ACI+branch%3Amaster -[codecov-img]: https://codecov.io/gh/quinnj/Structs.jl/branch/master/graph/badge.svg -[codecov-url]: https://codecov.io/gh/quinnj/Structs.jl -[issues-url]: https://github.com/quinnj/Structs.jl/issues +[ci-img]: https://github.com/quinnj/StructUtils.jl/workflows/CI/badge.svg +[ci-url]: https://github.com/quinnj/StructUtils.jl/actions?query=workflow%3ACI+branch%3Amaster +[codecov-img]: https://codecov.io/gh/quinnj/StructUtils.jl/branch/master/graph/badge.svg +[codecov-url]: https://codecov.io/gh/quinnj/StructUtils.jl +[issues-url]: https://github.com/quinnj/StructUtils.jl/issues diff --git a/src/Structs.jl b/src/Structs.jl index 3b80637..da9c3e0 100644 --- a/src/Structs.jl +++ b/src/Structs.jl @@ -1,23 +1,23 @@ -module Structs +module StructUtils using Dates, UUIDs export @noarg, @defaults, @tags """ - Structs.StructStyle + StructUtils.StructStyle Abstract type that all concrete struct styles must subtype. Custom struct styles allow fine-grained control over various -Structs.jl interface methods like `fieldtags`, `fielddefaults`, +StructUtils.jl interface methods like `fieldtags`, `fielddefaults`, `lift`, `lower`, etc. """ abstract type StructStyle end """ - Structs.DefaultStyle + StructUtils.DefaultStyle -Default struct style that all Structs.jl interface methods +Default struct style that all StructUtils.jl interface methods are defined for by default. """ struct DefaultStyle <: StructStyle end @@ -25,11 +25,11 @@ struct DefaultStyle <: StructStyle end include("macros.jl") """ - Structs.dictlike(::Type{T}) -> Bool - Structs.dictlike(::StructStyle, ::Type{T}) -> Bool + StructUtils.dictlike(::Type{T}) -> Bool + StructUtils.dictlike(::StructStyle, ::Type{T}) -> Bool Returns `true` if `T` is a dictionary-like type, `false` otherwise. -When `Structs.make(T, source)` is called, if `dictlike(T)` is `true`, +When `StructUtils.make(T, source)` is called, if `dictlike(T)` is `true`, an instance will be `initialize`d, and then `addkeyval!`ed for each key-value pair in `source`. """ @@ -42,8 +42,8 @@ dictlike(_, ::Type{T}) where {T} = dictlike(T) dictlike(st, ::T) where {T} = dictlike(st, T) """ - Structs.noarg(::Type{T}) -> Bool - Structs.noarg(::StructStyle, ::Type{T}) -> Bool + StructUtils.noarg(::Type{T}) -> Bool + StructUtils.noarg(::StructStyle, ::Type{T}) -> Bool Signals that `T` is a mutable type that can be constructed by calling an empty constructor, like `t = T()`. Automatically overloaded when structs use the @@ -57,15 +57,15 @@ noarg(_, ::Type{T}) where {T} = noarg(T) noarg(st, ::T) where {T} = noarg(st, T) """ - Structs.kwdef(::Type{T}) -> Bool - Structs.kwdef(::StructStyle, ::Type{T}) -> Bool + StructUtils.kwdef(::Type{T}) -> Bool + StructUtils.kwdef(::StructStyle, ::Type{T}) -> Bool Signals that `T` can be constructed by passing struct fields as keyword arguments to the constructor, like `t = T(field1=a, field2=b, ...)`. Automatically overloaded when structs use the `@kwdef` macro in their struct definition. The default value is `false` unless explicitly overloaded. -Note that `Structs.@kwdef` currently has no relation to `Base.@kwdef`, yet should +Note that `StructUtils.@kwdef` currently has no relation to `Base.@kwdef`, yet should be a drop-in replacement for it. """ function kwdef end @@ -75,7 +75,7 @@ kwdef(_, ::Type{T}) where {T} = kwdef(T) kwdef(st, ::T) where {T} = kwdef(st, T) """ - Structs.fieldtagkey(::Type{<:StructStyle}) -> Symbol + StructUtils.fieldtagkey(::Type{<:StructStyle}) -> Symbol Field tags defined on struct fields can be grouped by keys that are associated with a particular struct style. This function returns the key that should be used to @@ -85,7 +85,7 @@ retrieve field tags for a given struct style. By default, this function returns ```julia struct MySQLStyle <: StructStyle end -Structs.fieldtagkey(::Type{MySQLStyle}) = :mysql +StructUtils.fieldtagkey(::Type{MySQLStyle}) = :mysql @tags struct Foo a::Int &(mysql=(name="foo_a",),) @@ -93,7 +93,7 @@ Structs.fieldtagkey(::Type{MySQLStyle}) = :mysql end ``` -In this example, when `Structs.make` is called on `Foo` with the `MySQLStyle` style, +In this example, when `StructUtils.make` is called on `Foo` with the `MySQLStyle` style, only `(name="foo_a",)` will be retrieved from the field tags for `a` because the `mysql` key is associated with the `MySQLStyle` struct style. """ @@ -102,13 +102,13 @@ function fieldtagkey end fieldtagkey(::Type{T}) where {T} = nothing """ - Structs.fieldtags(::Type{T}) -> NamedTuple - Structs.fieldtags(::StructStyle, ::Type{T}) -> NamedTuple - Structs.fieldtags(::StructStyle, ::Type{T}, fieldname) -> NamedTuple + StructUtils.fieldtags(::Type{T}) -> NamedTuple + StructUtils.fieldtags(::StructStyle, ::Type{T}) -> NamedTuple + StructUtils.fieldtags(::StructStyle, ::Type{T}, fieldname) -> NamedTuple Returns a `NamedTuple` of field tags for the struct `T`. Field tags can be added manually by overloading `fieldtags`, or included via convenient syntax -using the Structs.jl macros: `@tags`, `@noarg`, `@defaults`, or `@kwdef`. +using the StructUtils.jl macros: `@tags`, `@noarg`, `@defaults`, or `@kwdef`. """ function fieldtags end @@ -126,13 +126,13 @@ fieldtags(::Type{T}, key) where {T} = get(() -> (;), fieldtags(T), key) end """ - Structs.fielddefaults(::Type{T}) -> NamedTuple - Structs.fielddefaults(::StructStyle, ::Type{T}) -> NamedTuple - Structs.fielddefault(::StructStyle, ::Type{T}, fieldname) -> NamedTuple + StructUtils.fielddefaults(::Type{T}) -> NamedTuple + StructUtils.fielddefaults(::StructStyle, ::Type{T}) -> NamedTuple + StructUtils.fielddefault(::StructStyle, ::Type{T}, fieldname) -> NamedTuple Returns a `NamedTuple` of field defaults for the struct `T`. Field defaults can be added manually by overloading `fielddefaults`, or included via convenient syntax -using the Structs.jl macros: `@tags`, `@noarg`, `@defaults`, or `@kwdef`. +using the StructUtils.jl macros: `@tags`, `@noarg`, `@defaults`, or `@kwdef`. """ function fielddefaults end @@ -143,12 +143,12 @@ fielddefault(st, ::Type{T}, key) where {T} = haskey(fielddefaults(st, T), key) ? @doc (@doc fielddefaults) fielddefault """ - Structs.initialize(T) -> T - Structs.initialize(T, dims) -> T - Structs.initialize(::StructStyle, T) -> T - Structs.initialize(::StructStyle, T, dims) -> T + StructUtils.initialize(T) -> T + StructUtils.initialize(T, dims) -> T + StructUtils.initialize(::StructStyle, T) -> T + StructUtils.initialize(::StructStyle, T, dims) -> T -In `Structs.make`, this function is called to initialize a new instance of `T`, +In `StructUtils.make`, this function is called to initialize a new instance of `T`, when `T` is `dictlike`, `arraylike`, or `noarg`. For `arraylike`, if the `source` in `make` is discovered to have multiple dimensions, `dims` will be passed to `initialize`. The default implementation of `initialize` is to call `T()` or `T(undef, dims...)` @@ -164,10 +164,10 @@ initialize(_, ::Type{T}) where {T} = initialize(T) initialize(_, ::Type{T}, dims) where {T} = initialize(T, dims) """ - Structs.addkeyval!(d, k, v) + StructUtils.addkeyval!(d, k, v) Add a key-value pair to a dictionary-like object `d`. This function is called -by `Structs.make` when `d` is `dictlike`. The default implementation is to +by `StructUtils.make` when `d` is `dictlike`. The default implementation is to call `d[k] = v` for `AbstractDict`. """ function addkeyval! end @@ -181,15 +181,15 @@ _valtype(d) = valtype(d) _valtype(::AbstractVector{Pair{A,B}}) where {A,B} = B """ - Structs.arraylike(::Type{T}) -> Bool - Structs.arraylike(::StructStyle, ::Type{T}) -> Bool + StructUtils.arraylike(::Type{T}) -> Bool + StructUtils.arraylike(::StructStyle, ::Type{T}) -> Bool Returns `true` if `T` is an array-like type, `false` otherwise. This function is -called by `Structs.make` to determine if `T` is array-like. The default +called by `StructUtils.make` to determine if `T` is array-like. The default implementation returns `true` for `<:AbstractArray`, `<:AbstractSet`, `<:Tuple`, `<:Base.Generator`, and `<:Core.SimpleVector` types, and `false` for `<:AbstractArray{T,0}`. -Once `initialize` is called, `Structs.make` will call `push!` to add values +Once `initialize` is called, `StructUtils.make` will call `push!` to add values to the array-like object. """ function arraylike end @@ -202,17 +202,17 @@ arraylike(::Type{<:Union{AbstractArray,AbstractSet,Tuple,Base.Generator,Core.Sim arraylike(::Type{<:AbstractArray{T,0}}) where {T} = false """ - Structs.structlike(::Type{T}) -> Bool - Structs.structlike(::StructStyle, ::Type{T}) -> Bool + StructUtils.structlike(::Type{T}) -> Bool + StructUtils.structlike(::StructStyle, ::Type{T}) -> Bool Returns `true` if `T` is a struct-like type, `false` otherwise. This function is -called by `Structs.make` to determine if `T` is struct-like. The default +called by `StructUtils.make` to determine if `T` is struct-like. The default implementation returns `true` for `isstructtype(T)` and `!Base.issingletontype(T)`. `structlike` structs are expected to be able to be constructed by the default constructor like `T(field1, field2, ...)`. -Due to how `Structs.make` works, `structlike` is often overloaded to `false` by "unit" types +Due to how `StructUtils.make` works, `structlike` is often overloaded to `false` by "unit" types where fields should be considered private the `make` process should instead attempt to `lift` the `source` object into the `unit` type. """ @@ -237,8 +237,8 @@ structlike(::Module) = false structlike(::Function) = false """ - Structs.nulllike(T) -> Bool - Structs.nulllike(::StructStyle, T) -> Bool + StructUtils.nulllike(T) -> Bool + StructUtils.nulllike(::StructStyle, T) -> Bool Returns `true` if `T` is a null-like type, `false` otherwise. This function is mainly used in the default `choosetype` implementation to determine if a @@ -254,11 +254,11 @@ nulllike(::Type{Nothing}) = true nulllike(::Type{Missing}) = true """ - Structs.lower(x) -> x - Structs.lower(::StructStyle, x) -> x + StructUtils.lower(x) -> x + StructUtils.lower(::StructStyle, x) -> x Domain value transformation function. This function is called by -`Structs.applyeach` on each value in the `source` object before +`StructUtils.applyeach` on each value in the `source` object before calling the apply function. By default, `lower` is the identity function. This allows a domain transformation of values according to the style used. @@ -279,10 +279,10 @@ function lower(st::StructStyle, x, tags) end """ - Structs.lift(::Type{T}, x) -> T - Structs.lift(::StructStyle, ::Type{T}, x) -> T + StructUtils.lift(::Type{T}, x) -> T + StructUtils.lift(::StructStyle, ::Type{T}, x) -> T -Lifts a value `x` to a type `T`. This function is called by `Structs.make` +Lifts a value `x` to a type `T`. This function is called by `StructUtils.make` to lift values to the appropriate type. The default implementation is the identity function for most types, but it also includes special cases for `Symbol`, `Char`, `UUID`, `VersionNumber`, `Regex`, and `TimeType` types. @@ -331,13 +331,13 @@ end @inline lift(f::F, st::StructStyle, ::Type{T}, x, tags) where {F,T} = f(lift(st, T, x, tags)) """ - Structs.choosetype(::Type{T}, x) -> T - Structs.choosetype(::StructStyle, ::Type{T}, x) -> T + StructUtils.choosetype(::Type{T}, x) -> T + StructUtils.choosetype(::StructStyle, ::Type{T}, x) -> T Chooses a concrete type from an abstract or union type `T` based on the value `x`, where -`x` is the "source" object in the context of `Structs.make`. +`x` is the "source" object in the context of `StructUtils.make`. This allows a runtime decision to be made around a concrete subtype -that should be used for `Structs.make` based on potentially dynamic values +that should be used for `StructUtils.make` based on potentially dynamic values of the source object. """ function choosetype end @@ -360,14 +360,14 @@ end @inline choosetype(f, style::StructStyle, ::Type{T}, x, tags) where {T} = f(style, choosetype(style, T, x, tags), x, tags) """ - Structs.applyeach(style, f, x) -> Union{Structs.EarlyReturn, Nothing} + StructUtils.applyeach(style, f, x) -> Union{StructUtils.EarlyReturn, Nothing} A custom `foreach`-like function that operates specifically on `(key, val)` or `(ind, val)` pairs, -and supports short-circuiting (via `Structs.EarlyReturn`). It also supports a `StructStyle` argument +and supports short-circuiting (via `StructUtils.EarlyReturn`). It also supports a `StructStyle` argument to allow for style-specific behavior for non-owned types. For each key-value or index-value pair in `x`, call `f(k, v)`. -If `f` returns a `Structs.EarlyReturn` instance, `applyeach` should +If `f` returns a `StructUtils.EarlyReturn` instance, `applyeach` should return the `EarlyReturn` immediately and stop iterating (i.e. short-circuit). Otherwise, the return value of `f` is ignored and iteration continues. @@ -376,11 +376,11 @@ Key types are generally expected to be Symbols, Strings, or Integers. An example overload of `applyeach` for a generic iterable would be: ```julia -function Structs.applyeach(style::Structs.StructStyle, f, x::MyIterable) +function StructUtils.applyeach(style::StructUtils.StructStyle, f, x::MyIterable) for (i, v) in enumerate(x) - ret = f(i, Structs.lower(style, v)) + ret = f(i, StructUtils.lower(style, v)) # if `f` returns EarlyReturn, return immediately - ret isa Structs.EarlyReturn && return ret + ret isa StructUtils.EarlyReturn && return ret end return end @@ -389,14 +389,14 @@ end Note that `applyeach` must include the `style` argument when overloading, even though it can be _called_ with out it, where the `DefaultStyle` will be used. -Also note that before applying `f`, the value `v` is passed through `Structs.lower(style, v)`. +Also note that before applying `f`, the value `v` is passed through `StructUtils.lower(style, v)`. If a value is `#undef` or otherwise not defined, the `f` function should be called with `nothing`. """ function applyeach end """ - Structs.EarlyReturn{T} + StructUtils.EarlyReturn{T} A wrapper type that can be used in function arguments to `applyeach` to short-circuit iteration and return a value from `applyeach`. @@ -406,9 +406,9 @@ Example usage: ```julia function find_needle_in_haystack(haystack, needle) ret = applyeach(haystack) do k, v - k == needle && return Structs.EarlyReturn(v) + k == needle && return StructUtils.EarlyReturn(v) end - ret isa Structs.EarlyReturn && return ret.value + ret isa StructUtils.EarlyReturn && return ret.value throw(ArgumentError("needle not found in haystack") end ```` @@ -745,7 +745,7 @@ function applylength(x) ref = Ref(0) lc = LengthClosure(Base.unsafe_convert(Ptr{Int}, ref)) GC.@preserve ref begin - Structs.applyeach(lc, x) + StructUtils.applyeach(lc, x) return unsafe_load(lc.len) end end @@ -794,10 +794,10 @@ end @inline (f::MultiDimValFunc{S,A})(x) where {S,A} = setindex!(f.arr, x, f.dims...) """ - Structs.make(T, source) -> T - Structs.make(style, T, source) -> T - Structs.make(f, style, T, source) -> nothing - Structs.make!(style, x::T, source) + StructUtils.make(T, source) -> T + StructUtils.make(style, T, source) -> T + StructUtils.make(f, style, T, source) -> nothing + StructUtils.make!(style, x::T, source) Construct a struct of type `T` from `source` using the given `style`. The `source` can be any type of object, and the `style` can be any `StructStyle` subtype. diff --git a/src/macros.jl b/src/macros.jl index 79025a6..d23317c 100644 --- a/src/macros.jl +++ b/src/macros.jl @@ -162,8 +162,8 @@ function parse_struct_def(kind, src, mod, expr) push!(expr.args[3].args, cexpr) #TODO: should we also generate an all-arg constructor like default struct constructors # that call convert to the field type for each field? - # override Structs.noarg(::Type{nm}) = true and add outside struct definition - push!(ret.args, :(Structs.noarg(::Type{<:$T}) = true)) + # override StructUtils.noarg(::Type{nm}) = true and add outside struct definition + push!(ret.args, :(StructUtils.noarg(::Type{<:$T}) = true)) generate_field_defaults_and_tags!(ret, T, fields) elseif kind == :kwdef if !isempty(fields) @@ -179,8 +179,8 @@ function parse_struct_def(kind, src, mod, expr) push!(ret.args, fexpr) end end - # override Structs.kwdef(::Type{T}) = true and add outside struct definition - push!(ret.args, :(Structs.kwdef(::Type{<:$T}) = true)) + # override StructUtils.kwdef(::Type{T}) = true and add outside struct definition + push!(ret.args, :(StructUtils.kwdef(::Type{<:$T}) = true)) generate_field_defaults_and_tags!(ret, T, fields) else # if any default are specified, ensure all trailing fields have defaults @@ -207,12 +207,12 @@ function generate_field_defaults_and_tags!(ret, T, fields) # generate fielddefaults override if applicable if any(f.default !== none for f in fields) defs_nt = Expr(:tuple, Expr(:parameters, [:(($(f.name)=$(f.default))) for f in fields if f.default !== none]...)) - push!(ret.args, :(Structs.fielddefaults(::Type{<:$T}) = $defs_nt)) + push!(ret.args, :(StructUtils.fielddefaults(::Type{<:$T}) = $defs_nt)) end # generate fieldtags override if applicable if any(f.tags !== none for f in fields) tags_nt = Expr(:tuple, Expr(:parameters, [:($(f.name)=$(f.tags)) for f in fields if f.tags !== none]...)) - push!(ret.args, :(Structs.fieldtags(::Type{<:$T}) = $tags_nt)) + push!(ret.args, :(StructUtils.fieldtags(::Type{<:$T}) = $tags_nt)) end end @@ -220,15 +220,15 @@ const SHARED_MACRO_DOCS = """ The `@noarg`, `@kwdef`, `@defaults`, and `@tags` macros all support specifying "field tags" for each field in a struct. Field tags are a NamedTuple prefixed by `&` and are a way to attach metadata to a field. The -field tags are accessible via the `Structs.fieldtags` function, and certain -field tags are used by the `Structs.make` function to control how fields are +field tags are accessible via the `StructUtils.fieldtags` function, and certain +field tags are used by the `StructUtils.make` function to control how fields are constructed, including: * `dateformat`: a `DateFormat` object to use when parsing or formatting a `Dates.TimeType` field * `lower`: a function to apply to a field when `applyeach` is called on a struct - * `lift`: a function to apply to a field when `Structs.make` is called on for a struct + * `lift`: a function to apply to a field when `StructUtils.make` is called on for a struct * `ignore`: a `Bool` to indicate if a field should be skipped/ignored when `applyeach` or `make` is called * `name`: a `Symbol` to be used instead of a defined field name in `applyeach` or used to match a field in `make` - * `choosetype`: a function to apply to a field when `Structs.make` is called to determine the concrete type of an abstract or Union typed field + * `choosetype`: a function to apply to a field when `StructUtils.make` is called to determine the concrete type of an abstract or Union typed field For example, the following struct definition includes a field with a `dateformat` tag: ```julia @@ -246,10 +246,10 @@ end Macro to enhance a `mutable struct` definition by automatically generating an empty or "no-argument" constructor. Similar to the `@kwdef` macro, default values can be specified for fields, which will -be set in the generated constructor. `Structs.noarg` trait is also +be set in the generated constructor. `StructUtils.noarg` trait is also overridden to return `true` for the struct type. This allows structs to easily participate in programmatic construction via -`Structs.make`. +`StructUtils.make`. Note that `const` fields are currently not allowed in `@noarg` structs. @@ -286,9 +286,9 @@ end Macro to enhance a `struct` definition by automatically generating a keyword argument constructor. Default values can be specified for fields, -which will be set in the generated constructor. `Structs.kwdef` trait is +which will be set in the generated constructor. `StructUtils.kwdef` trait is also overridden to return `true` for the struct type. This allows structs -to easily participate in programmatic construction via `Structs.make`. +to easily participate in programmatic construction via `StructUtils.make`. $SHARED_MACRO_DOCS @@ -321,7 +321,7 @@ end Macro to enhance a `struct` definition by automatically generating an outer constructor with default values for trailing fields. The generated constructor will accept arguments for non-default fields and pass default -values to the inner constructor. `Structs.fielddefaults` trait is also +values to the inner constructor. `StructUtils.fielddefaults` trait is also overridden to return a `NamedTuple` of default values for the struct type. $SHARED_MACRO_DOCS diff --git a/src/selectors.jl b/src/selectors.jl new file mode 100644 index 0000000..2730649 --- /dev/null +++ b/src/selectors.jl @@ -0,0 +1,214 @@ +""" + Selection syntax + +Special "selection syntax" is provided that allows easy querying of objects/arrays that implement `StructUtils.applyeach` using a syntax similar to XPath or CSS selectors, +applied using common Julia syntax. + +This syntax mainly uses various forms of `getindex` to select elements of an object or array. +Supported syntax includes: + * `x["key"]` / `x.key` / `x[:key]` / `x[1]` - select the value associated for a key in object `x` (key can be a String, Symbol, or Integer for an array) + * `x[:]` - select all values in object or array `x`, returned as a `Selectors.List`, which is a custom array type that supports the selection syntax + * `x.key` - when `x` is a `List`, select the value for `key` in each element of the `List` (like a broadcasted `getindex`) + * `x[~, key]` - recursively select all values in object or array `x` that have `key` + * `x[~, :]` - recursively select all values in object or array `x`, returned as a flattened `List` + * `x[:, (k, v) -> Bool]` - apply a key-value function `f` to each key-value/index-value in object or array `x`, and return a `List` of all values for which `f` returns `true` +""" +module Selectors + +using ..StructUtils + +export List, @selectors + +""" + List(...) + +A custom array wrapper that supports the Selectors selection syntax. +""" +struct List{T} <: AbstractVector{T} + items::Vector{T} +end + +items(x::List) = getfield(x, :items) +Base.getindex(x::List) = map(getindex, items(x)) +List(T=Any) = List(T[]) +Base.size(x::List) = size(items(x)) +Base.eltype(::List{T}) where {T} = T +Base.isassigned(x::List, args::Integer...) = isassigned(items(x), args...) + +Base.push!(x::List, item) = push!(items(x), item) +Base.append!(x::List, items_to_append) = append!(items(x), items_to_append) + +StructUtils.arraylike(::List) = true + +function StructUtils.applyeach(f, x::List) + # note that there should *never* be #undef + # values in a list, since we only ever initialize empty + # then push!/append! to it + for (i, v) in enumerate(items(x)) + ret = f(i, v) + ret isa StructUtils.EarlyReturn && return ret + end + return +end + +const KeyInd = Union{AbstractString, Symbol} +const Inds = Union{AbstractVector{<:KeyInd}, NTuple{N, <:KeyInd} where {N}, + AbstractVector{<:Integer}, NTuple{N, <:Integer} where {N}} + +function _getindex(x, key::Union{KeyInd, Integer}) + if StructUtils.arraylike(x) && key isa KeyInd + # indexing an array with a key, so we check + # each element if it's an object and if the + # object has the key + # like a broadcasted getindex over x + values = List() + StructUtils.applyeach(x) do _, item + if StructUtils.structlike(item) + # if array elements are objects, we do a broadcasted getproperty with `key` + # should we try-catch and ignore KeyErrors? + push!(values, _getindex(item, key)) + else + # non-objects are just ignored + end + return + end + return values + elseif StructUtils.structlike(x) || StructUtils.arraylike(x) + # indexing object w/ key or array w/ index + # returns a single value + ret = StructUtils.applyeach(x) do k, v + StructUtils.keyeq(k, key) && return StructUtils.EarlyReturn(v) + return + end + ret isa StructUtils.EarlyReturn || throw(KeyError(key)) + return ret.value + else + noselection(x) + end +end + +# return all values of an object or elements of an array as a List +function _getindex(x, ::Colon) + selectioncheck(x) + values = List() + StructUtils.applyeach(x) do _, v + push!(values, v) + return + end + return values +end + +# a list is already a list of all its elements +_getindex(x::List, ::Colon) = x + +# indexing object or array w/ a list of keys/indexes +function _getindex(x, inds::Inds) + selectioncheck(x) + values = List() + StructUtils.applyeach(x) do k, v + i = findfirst(StructUtils.keyeq(k), inds) + i !== nothing && push!(values, v) + return + end + return values +end + +# return all values of an object or elements of an array as a List +# that satisfy a key-value function +function _getindex(x, S::Union{typeof(~), Colon}, f::Base.Callable) + selectioncheck(x) + values = List() + StructUtils.applyeach(x) do k, v + f(k, v) && push!(values, v) + if S == ~ + if StructUtils.structlike(v) + ret = _getindex(v, ~, f) + append!(values, ret) + elseif StructUtils.arraylike(v) + ret = _getindex(v, ~, f) + append!(values, ret) + end + end + return + end + return values +end + +# recursively return all values of an object or elements of an array as a List (:) +# as a single flattened List; or all properties that match key +function _getindex(x, ::typeof(~), key::Union{KeyInd, Colon}) + values = List() + if StructUtils.structlike(x) + StructUtils.applyeach(x) do k, v + if key === Colon() + push!(values, v) + elseif StructUtils.keyeq(k, key) + if StructUtils.arraylike(v) + StructUtils.applyeach(v) do _, vv + push!(values, vv) + return + end + else + push!(values, v) + end + end + if StructUtils.structlike(v) + ret = _getindex(v, ~, key) + append!(values, ret) + elseif StructUtils.arraylike(v) + ret = _getindex(v, ~, key) + append!(values, ret) + end + return + end + elseif StructUtils.arraylike(x) + StructUtils.applyeach(x) do _, item + if StructUtils.structlike(item) + ret = _getindex(item, ~, key) + append!(values, ret) + elseif StructUtils.arraylike(item) + ret = _getindex(item, ~, key) + append!(values, ret) + end + return + end + else + noselection(x) + end + return values +end + +selectioncheck(x) = StructUtils.structlike(x) || StructUtils.arraylike(x) || noselection(x) +@noinline noselection(x) = throw(ArgumentError("Selection syntax not defined for this object of type: `$(typeof(x))`")) + +tosymbol(x::Symbol) = x +tosymbol(x) = Symbol(x) + +# build up propertynames by iterating over each key-value pair +function _propertynames(x) + selectioncheck(x) + nms = Symbol[] + StructUtils.applyeach(x) do k, _ + push!(nms, tosymbol(k)) + return + end + return nms +end + +# convenience macro for defining high-level getindex/getproperty methods +macro selectors(T) + esc(quote + Base.getindex(x::$T, arg) = StructUtils.Selectors._getindex(x, arg) + Base.getindex(x::$T, ::Colon, arg) = StructUtils.Selectors._getindex(x, :, arg) + Base.getindex(x::$T, ::typeof(~), arg) = StructUtils.Selectors._getindex(x, ~, arg) + Base.getindex(x::$T, ::typeof(~), key, val) = StructUtils.Selectors._getindex(x, ~, key, val) + Base.getproperty(x::$T, key::Symbol) = StructUtils.Selectors._getindex(x, key) + Base.propertynames(x::$T) = StructUtils.Selectors._propertynames(x) + Base.hasproperty(x::$T, key::Symbol) = key in propertynames(x) + Base.length(x::$T) = StructUtils.applylength(x) + end) +end + +@selectors List + +end # module \ No newline at end of file diff --git a/test/macros.jl b/test/macros.jl index 210eae4..81864d3 100644 --- a/test/macros.jl +++ b/test/macros.jl @@ -36,7 +36,7 @@ Documentation for NoArg4 a::String end -Structs.@kwdef mutable struct KwDef1 +StructUtils.@kwdef mutable struct KwDef1 no_type with_type::Int with_default = 1 @@ -63,32 +63,32 @@ Structs.@kwdef mutable struct KwDef1 const with_tag_type_default_const::Int = 1 &(xml=(key="with-tag-default-const",),) end -Structs.@kwdef struct NoFields +StructUtils.@kwdef struct NoFields end -Structs.@kwdef struct KwDef2{T, S <: IO} <: AbstractNoArg +StructUtils.@kwdef struct KwDef2{T, S <: IO} <: AbstractNoArg a::T = 10 * 20 io::S end # tests from Base.@kwdef misc.jl -Structs.@kwdef struct Test27970Typed +StructUtils.@kwdef struct Test27970Typed a::Int b::String = "hi" end -Structs.@kwdef struct Test27970Untyped +StructUtils.@kwdef struct Test27970Untyped a end -Structs.@kwdef struct Test27970Empty end +StructUtils.@kwdef struct Test27970Empty end abstract type AbstractTest29307 end -Structs.@kwdef struct Test29307{T<:Integer} <: AbstractTest29307 +StructUtils.@kwdef struct Test29307{T<:Integer} <: AbstractTest29307 a::T=2 end -Structs.@kwdef struct TestInnerConstructor +StructUtils.@kwdef struct TestInnerConstructor a = 1 TestInnerConstructor(a::Int) = (@assert a>0; new(a)) function TestInnerConstructor(a::String) @@ -98,12 +98,12 @@ Structs.@kwdef struct TestInnerConstructor end const outsidevar = 7 -Structs.@kwdef struct TestOutsideVar +StructUtils.@kwdef struct TestOutsideVar a::Int=outsidevar end @test TestOutsideVar() == TestOutsideVar(7) -Structs.@kwdef mutable struct Test_kwdef_const_atomic +StructUtils.@kwdef mutable struct Test_kwdef_const_atomic a b::Int c::Int = 1 @@ -114,16 +114,16 @@ Structs.@kwdef mutable struct Test_kwdef_const_atomic @atomic h::Int end -Structs.@kwdef struct Test_kwdef_lineinfo +StructUtils.@kwdef struct Test_kwdef_lineinfo a::String end -# @testset "Structs.@kwdef constructor line info" begin +# @testset "StructUtils.@kwdef constructor line info" begin # for method in methods(Test_kwdef_lineinfo) # @test method.file === Symbol(@__FILE__) # @test ((@__LINE__)-6) ≤ method.line ≤ ((@__LINE__)-5) # end # end -Structs.@kwdef struct Test_kwdef_lineinfo_sparam{S<:AbstractString} +StructUtils.@kwdef struct Test_kwdef_lineinfo_sparam{S<:AbstractString} a::S end # @testset "@kwdef constructor line info with static parameter" begin @@ -134,12 +134,12 @@ end # end module KwdefWithEsc - using Structs + using StructUtils const Int1 = Int const val1 = 42 macro define_struct() quote - Structs.@kwdef struct $(esc(:Struct)) + StructUtils.@kwdef struct $(esc(:Struct)) a b = val1 c::Int1 @@ -197,29 +197,29 @@ end x = NoArg2() x.no_type = :hey @test x isa NoArg2 - @test Structs.noarg(NoArg2) - fd = Structs.fielddefaults(NoArg2) + @test StructUtils.noarg(NoArg2) + fd = StructUtils.fielddefaults(NoArg2) @test fd.with_default == 1 && fd.with_type_default == 1 && fd.with_tag_default == 1 && fd.with_tag_type_default == 1 && fd.with_default_atomic == 1 && fd.with_type_default_atomic == 1 && fd.with_tag_default_atomic == 1 && fd.with_tag_type_default_atomic == 1 - ft = Structs.fieldtags(NoArg2) + ft = StructUtils.fieldtags(NoArg2) @test ft.with_tag == (xml=(key="with-tag",),) && ft.with_tag_type == (xml=(key="with-tag-type",),) && ft.with_tag_default == (xml=(key="with-tag-default",),) && ft.with_tag_type_default == (xml=(key="with-tag-default",),) && ft.with_tag_atomic == (xml=(key="with-tag-atomic",),) && ft.with_tag_type_atomic == (xml=(key="with-tag-type-atomic",),) && ft.with_tag_default_atomic == (xml=(key="with-tag-default-atomic",),) && ft.with_tag_type_default_atomic == (xml=(key="with-tag-default-atomic",),) @test string(@doc(NoArg4)) == "Documentation for NoArg4\n" - @test_throws ArgumentError @macroexpand Structs.@noarg struct NonMutableNoArg + @test_throws ArgumentError @macroexpand StructUtils.@noarg struct NonMutableNoArg with_type::Int = 1 end # can't use @noarg w/ struct w/ const fields - @test_throws ArgumentError @macroexpand Structs.@noarg mutable struct NoArgConst + @test_throws ArgumentError @macroexpand StructUtils.@noarg mutable struct NoArgConst const a::Int = 1 end x = KwDef1(no_type=1, with_type=1, with_tag=1, with_tag_type=1, no_type_atomic=1, with_type_atomic=1, with_tag_atomic=1, with_tag_type_atomic=1, no_type_const=1, with_type_const=1, with_tag_const=1, with_tag_type_const=1) @test x.no_type == 1 - @test Structs.kwdef(KwDef1) - fd = Structs.fielddefaults(KwDef1) + @test StructUtils.kwdef(KwDef1) + fd = StructUtils.fielddefaults(KwDef1) @test fd.with_default == 1 && fd.with_type_default == 1 && fd.with_tag_default == 1 && fd.with_tag_type_default == 1 && fd.with_default_atomic == 1 && fd.with_type_default_atomic == 1 && fd.with_tag_default_atomic == 1 && fd.with_tag_type_default_atomic == 1 && fd.with_default_const == 1 && fd.with_type_default_const == 1 && fd.with_tag_default_const == 1 && fd.with_tag_type_default_const == 1 - ft = Structs.fieldtags(KwDef1) + ft = StructUtils.fieldtags(KwDef1) @test ft.with_tag == (xml=(key="with-tag",),) && ft.with_tag_type == (xml=(key="with-tag-type",),) && ft.with_tag_default == (xml=(key="with-tag-default",),) && ft.with_tag_type_default == (xml=(key="with-tag-default",),) && ft.with_tag_atomic == (xml=(key="with-tag-atomic",),) && ft.with_tag_type_atomic == (xml=(key="with-tag-type-atomic",),) && ft.with_tag_default_atomic == (xml=(key="with-tag-default-atomic",),) && ft.with_tag_type_default_atomic == (xml=(key="with-tag-default-atomic",),) && ft.with_tag_const == (xml=(key="with-tag-const",),) && ft.with_tag_type_const == (xml=(key="with-tag-type-const",),) && ft.with_tag_default_const == (xml=(key="with-tag-default-const",),) && ft.with_tag_type_default_const == (xml=(key="with-tag-default-const",),) @test NoFields() isa NoFields diff --git a/test/runtests.jl b/test/runtests.jl index d02483b..bcc597a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,11 +1,11 @@ -using Test, Dates, UUIDs, Structs +using Test, Dates, UUIDs, StructUtils -struct TestStyle <: Structs.StructStyle end +struct TestStyle <: StructUtils.StructStyle end -include(joinpath(dirname(pathof(Structs)), "../test/macros.jl")) -include(joinpath(dirname(pathof(Structs)), "../test/struct.jl")) +include(joinpath(dirname(pathof(StructUtils)), "../test/macros.jl")) +include(joinpath(dirname(pathof(StructUtils)), "../test/struct.jl")) -@testset "Structs" begin +@testset "StructUtils" begin # Dict{Symbol, Int}, NamedTuple, A struct, Vector Pair d = Dict(:a => 1, :b => 2, :c => 3, :d => 4) @@ -20,153 +20,153 @@ v = [1, 2, 3, 4] t = (1, 2, 3, 4) println("Dict{Symbol, Int}") -@test Structs.make(Dict{Symbol, Int}, d) == d -@test Structs.make(Dict{Symbol, Int}, ds) == d -@test Structs.make(Dict{Symbol, Int}, nt) == d -@test Structs.make(Dict{Symbol, Int}, a) == d -@test Structs.make(Dict{Symbol, Int}, vp) == d -# @test Structs.make(Dict{Symbol, Int}, aa) == d # fails because of extra field -@test Structs.make(Dict{Symbol, Int}, v) == Dict(Symbol(1) => 1, Symbol(2) => 2, Symbol(3) => 3, Symbol(4) => 4) -@test Structs.make(Dict{Symbol, Int}, t) == Dict(Symbol(1) => 1, Symbol(2) => 2, Symbol(3) => 3, Symbol(4) => 4) -@test Structs.make(Dict{Symbol, Int}, b) == d -@test Structs.make(Dict{Symbol, Int}, bb) == d +@test StructUtils.make(Dict{Symbol, Int}, d) == d +@test StructUtils.make(Dict{Symbol, Int}, ds) == d +@test StructUtils.make(Dict{Symbol, Int}, nt) == d +@test StructUtils.make(Dict{Symbol, Int}, a) == d +@test StructUtils.make(Dict{Symbol, Int}, vp) == d +# @test StructUtils.make(Dict{Symbol, Int}, aa) == d # fails because of extra field +@test StructUtils.make(Dict{Symbol, Int}, v) == Dict(Symbol(1) => 1, Symbol(2) => 2, Symbol(3) => 3, Symbol(4) => 4) +@test StructUtils.make(Dict{Symbol, Int}, t) == Dict(Symbol(1) => 1, Symbol(2) => 2, Symbol(3) => 3, Symbol(4) => 4) +@test StructUtils.make(Dict{Symbol, Int}, b) == d +@test StructUtils.make(Dict{Symbol, Int}, bb) == d println("NamedTuple") -@test Structs.make(typeof(nt), d) == nt -@test Structs.make(typeof(nt), ds) == nt -@test Structs.make(typeof(nt), nt) == nt -@test Structs.make(typeof(nt), a) == nt -@test Structs.make(typeof(nt), vp) == nt -@test Structs.make(typeof(nt), v) == nt -@test Structs.make(typeof(nt), t) == nt -@test Structs.make(typeof(nt), aa) == nt # extra field is ignored -@test Structs.make(typeof(nt), b) == nt -@test Structs.make(typeof(nt), bb) == nt +@test StructUtils.make(typeof(nt), d) == nt +@test StructUtils.make(typeof(nt), ds) == nt +@test StructUtils.make(typeof(nt), nt) == nt +@test StructUtils.make(typeof(nt), a) == nt +@test StructUtils.make(typeof(nt), vp) == nt +@test StructUtils.make(typeof(nt), v) == nt +@test StructUtils.make(typeof(nt), t) == nt +@test StructUtils.make(typeof(nt), aa) == nt # extra field is ignored +@test StructUtils.make(typeof(nt), b) == nt +@test StructUtils.make(typeof(nt), bb) == nt println("A struct") -@test Structs.make(A, d) == a -@test Structs.make(A, ds) == a # slower -@test Structs.make(A, nt) == a -@test Structs.make(A, a) == a -@test Structs.make(A, vp) == a -@test Structs.make(A, v) == a # relies on order of vector elements -@test Structs.make(A, t) == a # relies on order of tuple elements -@test Structs.make(A, aa) == a # extra field is ignored -@test Structs.make(A, b) == a -@test Structs.make(A, bb) == a +@test StructUtils.make(A, d) == a +@test StructUtils.make(A, ds) == a # slower +@test StructUtils.make(A, nt) == a +@test StructUtils.make(A, a) == a +@test StructUtils.make(A, vp) == a +@test StructUtils.make(A, v) == a # relies on order of vector elements +@test StructUtils.make(A, t) == a # relies on order of tuple elements +@test StructUtils.make(A, aa) == a # extra field is ignored +@test StructUtils.make(A, b) == a +@test StructUtils.make(A, bb) == a println("AA struct") -@test Structs.make(AA, d) == aa # works because of AA field e default -@test Structs.make(AA, ds) == aa -@test Structs.make(AA, nt) == aa -@test Structs.make(AA, a) == aa -@test Structs.make(AA, vp) == aa -@test Structs.make(AA, v) == aa # relies on order of vector elements -@test Structs.make(AA, t) == aa # relies on order of tuple elements -@test Structs.make(AA, aa) == aa # extra field is ignored -@test Structs.make(AA, b) == aa -@test Structs.make(AA, bb) == aa +@test StructUtils.make(AA, d) == aa # works because of AA field e default +@test StructUtils.make(AA, ds) == aa +@test StructUtils.make(AA, nt) == aa +@test StructUtils.make(AA, a) == aa +@test StructUtils.make(AA, vp) == aa +@test StructUtils.make(AA, v) == aa # relies on order of vector elements +@test StructUtils.make(AA, t) == aa # relies on order of tuple elements +@test StructUtils.make(AA, aa) == aa # extra field is ignored +@test StructUtils.make(AA, b) == aa +@test StructUtils.make(AA, bb) == aa println("B struct") -@test Structs.make(B, d) == b -@test Structs.make(B, ds) == b -@test Structs.make(B, nt) == b -@test Structs.make(B, a) == b -@test Structs.make(B, vp) == b -@test Structs.make(B, v) == b # relies on order of vector elements -@test Structs.make(B, t) == b # relies on order of tuple elements -@test Structs.make(B, aa) == b # extra field is ignored -@test Structs.make(B, b) == b -@test Structs.make(B, bb) == b +@test StructUtils.make(B, d) == b +@test StructUtils.make(B, ds) == b +@test StructUtils.make(B, nt) == b +@test StructUtils.make(B, a) == b +@test StructUtils.make(B, vp) == b +@test StructUtils.make(B, v) == b # relies on order of vector elements +@test StructUtils.make(B, t) == b # relies on order of tuple elements +@test StructUtils.make(B, aa) == b # extra field is ignored +@test StructUtils.make(B, b) == b +@test StructUtils.make(B, bb) == b println("BB struct") -@test Structs.make(BB, d) == bb -@test Structs.make(BB, ds) == bb -@test Structs.make(BB, nt) == bb -@test Structs.make(BB, a) == bb -@test Structs.make(BB, vp) == bb -@test Structs.make(BB, v) == bb # relies on order of vector elements -@test Structs.make(BB, t) == bb # relies on order of tuple elements -@test Structs.make(BB, aa) == bb # extra field is ignored -@test Structs.make(BB, b) == bb -@test Structs.make(BB, bb) == bb +@test StructUtils.make(BB, d) == bb +@test StructUtils.make(BB, ds) == bb +@test StructUtils.make(BB, nt) == bb +@test StructUtils.make(BB, a) == bb +@test StructUtils.make(BB, vp) == bb +@test StructUtils.make(BB, v) == bb # relies on order of vector elements +@test StructUtils.make(BB, t) == bb # relies on order of tuple elements +@test StructUtils.make(BB, aa) == bb # extra field is ignored +@test StructUtils.make(BB, b) == bb +@test StructUtils.make(BB, bb) == bb println("Vector Pair") -# @test Structs.make(typeof(vp), d) == vp # unordered Dict doesn't work -# @test Structs.make(typeof(vp), ds) == vp # unordered Dict doesn't work -@test Structs.make(typeof(vp), nt) == vp -@test Structs.make(typeof(vp), a) == vp -@test Structs.make(typeof(vp), vp) == vp +# @test StructUtils.make(typeof(vp), d) == vp # unordered Dict doesn't work +# @test StructUtils.make(typeof(vp), ds) == vp # unordered Dict doesn't work +@test StructUtils.make(typeof(vp), nt) == vp +@test StructUtils.make(typeof(vp), a) == vp +@test StructUtils.make(typeof(vp), vp) == vp println("Vector") -# @test Structs.make(typeof(v), d) == v # relies on order of Dict elements -# @test Structs.make(typeof(v), ds) == v # relies on order of Dict elements -@test Structs.make(typeof(v), nt) == v -@test Structs.make(typeof(v), a) == v -@test Structs.make(typeof(v), vp) == v -@test Structs.make(typeof(v), v) == v -@test Structs.make(typeof(v), t) == v -# @test Structs.make(typeof(v), aa) == v # fails because of extra field +# @test StructUtils.make(typeof(v), d) == v # relies on order of Dict elements +# @test StructUtils.make(typeof(v), ds) == v # relies on order of Dict elements +@test StructUtils.make(typeof(v), nt) == v +@test StructUtils.make(typeof(v), a) == v +@test StructUtils.make(typeof(v), vp) == v +@test StructUtils.make(typeof(v), v) == v +@test StructUtils.make(typeof(v), t) == v +# @test StructUtils.make(typeof(v), aa) == v # fails because of extra field println("Tuple") -# @test Structs.make(typeof(t), d) == t # relies on order of Dict elements -# @test Structs.make(typeof(t), ds) == t # relies on order of Dict elements -@test Structs.make(typeof(t), nt) == t -@test Structs.make(typeof(t), a) == t -@test Structs.make(typeof(t), vp) == t -@test Structs.make(typeof(t), v) == t -@test Structs.make(typeof(t), t) == t -@test Structs.make(typeof(t), aa) == t +# @test StructUtils.make(typeof(t), d) == t # relies on order of Dict elements +# @test StructUtils.make(typeof(t), ds) == t # relies on order of Dict elements +@test StructUtils.make(typeof(t), nt) == t +@test StructUtils.make(typeof(t), a) == t +@test StructUtils.make(typeof(t), vp) == t +@test StructUtils.make(typeof(t), v) == t +@test StructUtils.make(typeof(t), t) == t +@test StructUtils.make(typeof(t), aa) == t println("C") -@test Structs.make(C, ()) == C() -@test Structs.make(C, (1,)) == C() +@test StructUtils.make(C, ()) == C() +@test StructUtils.make(C, (1,)) == C() println("D") -@test Structs.make(D, (1, 3.14, "hey")) == D(1, 3.14, "hey") +@test StructUtils.make(D, (1, 3.14, "hey")) == D(1, 3.14, "hey") println("Wrapper") -@test Structs.make(Wrapper, Dict("x" => Dict("a" => 1, "b" => "hey"))) == Wrapper((a=1, b="hey")) +@test StructUtils.make(Wrapper, Dict("x" => Dict("a" => 1, "b" => "hey"))) == Wrapper((a=1, b="hey")) println("UndefGuy") -x = Structs.make(UndefGuy, (id=1, name="2")) +x = StructUtils.make(UndefGuy, (id=1, name="2")) @test x.id == 1 && x.name == "2" -x = Structs.make(UndefGuy, (id=1,)) +x = StructUtils.make(UndefGuy, (id=1,)) @test x.id == 1 && !isdefined(x, :name) println("E") -@test Structs.make(E, Dict("id" => 1, "a" => (a=1, b=2, c=3, d=4))) == E(1, A(1, 2, 3, 4)) +@test StructUtils.make(E, Dict("id" => 1, "a" => (a=1, b=2, c=3, d=4))) == E(1, A(1, 2, 3, 4)) println("G") -@test Structs.make(G, Dict("id" => 1, "rate" => 3.14, "name" => "Jim", "f" => Dict("id" => 2, "rate" => 6.28, "name" => "Bob"))) == G(1, 3.14, "Jim", F(2, 6.28, "Bob")) +@test StructUtils.make(G, Dict("id" => 1, "rate" => 3.14, "name" => "Jim", "f" => Dict("id" => 2, "rate" => 6.28, "name" => "Bob"))) == G(1, 3.14, "Jim", F(2, 6.28, "Bob")) println("H") -x = Structs.make(H, (id=0, name="", properties=Dict("a" => 1), addresses=["a", "b", "c"])) +x = StructUtils.make(H, (id=0, name="", properties=Dict("a" => 1), addresses=["a", "b", "c"])) @test x.id == 0 && x.name == "" && x.properties == Dict("a" => 1) && x.addresses == ["a", "b", "c"] println("I") -@test Structs.make(I, (id=2, name="Aubrey", fruit=apple)) == I(2, "Aubrey", apple) +@test StructUtils.make(I, (id=2, name="Aubrey", fruit=apple)) == I(2, "Aubrey", apple) println("Vehicle") -Structs.choosetype(::Type{Vehicle}, source) = source["type"] == "car" ? Car : Truck -x = Structs.make(Vehicle, Dict("type" => "car", "make" => "Toyota", "model" => "Corolla", "seatingCapacity" => 4, "topSpeed" => 120.5)) +StructUtils.choosetype(::Type{Vehicle}, source) = source["type"] == "car" ? Car : Truck +x = StructUtils.make(Vehicle, Dict("type" => "car", "make" => "Toyota", "model" => "Corolla", "seatingCapacity" => 4, "topSpeed" => 120.5)) @test x == Car("Toyota", "Corolla", 4, 120.5) println("J") -@test Structs.make(J, (id=1, name=nothing, rate=3.14)) == J(1, nothing, 3.14) -@test Structs.make(J, (id=nothing, name=nothing, rate=3)) == J(nothing, nothing, 3) +@test StructUtils.make(J, (id=1, name=nothing, rate=3.14)) == J(1, nothing, 3.14) +@test StructUtils.make(J, (id=nothing, name=nothing, rate=3)) == J(nothing, nothing, 3) println("Recurs") -@test Structs.make(Recurs, (id=0, value=(id=1, value=(id=2, value=(id=3, value=(id=4, value=nothing)))))) == Recurs(0, Recurs(1, Recurs(2, Recurs(3, Recurs(4, nothing))))) +@test StructUtils.make(Recurs, (id=0, value=(id=1, value=(id=2, value=(id=3, value=(id=4, value=nothing)))))) == Recurs(0, Recurs(1, Recurs(2, Recurs(3, Recurs(4, nothing))))) println("O") -@test Structs.make(O, (id=0, name=missing)) == O(0, missing) -@test Structs.make(O, (id=0, name=nothing)) == O(0, nothing) -@test Structs.make(O, (id=0, name=(id=2, first_name="Jim", rate=3.14))) == O(0, L(2, "Jim", 3.14)) -@test Structs.make(O, (id=0, name=(id=2, name="Jane", fruit=banana))) == O(0, I(2, "Jane", banana)) +@test StructUtils.make(O, (id=0, name=missing)) == O(0, missing) +@test StructUtils.make(O, (id=0, name=nothing)) == O(0, nothing) +@test StructUtils.make(O, (id=0, name=(id=2, first_name="Jim", rate=3.14))) == O(0, L(2, "Jim", 3.14)) +@test StructUtils.make(O, (id=0, name=(id=2, name="Jane", fruit=banana))) == O(0, I(2, "Jane", banana)) println("P") -p = Structs.make(P, (id=0, name="Jane")) +p = StructUtils.make(P, (id=0, name="Jane")) @test p.id == 0 && p.name == "Jane" end \ No newline at end of file diff --git a/test/struct.jl b/test/struct.jl index 643e99e..153a485 100644 --- a/test/struct.jl +++ b/test/struct.jl @@ -13,7 +13,7 @@ struct AA e::Int end -Structs.fielddefaults(::Type{AA}) = (e=5,) +StructUtils.fielddefaults(::Type{AA}) = (e=5,) mutable struct B a::Int @@ -23,7 +23,7 @@ mutable struct B B() = new() end -Structs.noarg(::Type{B}) = true +StructUtils.noarg(::Type{B}) = true Base.:(==)(b1::B, b2::B) = b1.a == b2.a && b1.b == b2.b && b1.c == b2.c && b1.d == b2.d Base.@kwdef struct BB @@ -33,7 +33,7 @@ Base.@kwdef struct BB d::Int = 4 end -Structs.kwdef(::Type{BB}) = true +StructUtils.kwdef(::Type{BB}) = true struct C end @@ -92,7 +92,7 @@ mutable struct UndefGuy UndefGuy() = new() end -Structs.noarg(::Type{UndefGuy}) = true +StructUtils.noarg(::Type{UndefGuy}) = true struct E id::Int @@ -105,7 +105,7 @@ Base.@kwdef struct F name::String end -Structs.kwdef(::Type{F}) = true +StructUtils.kwdef(::Type{F}) = true Base.@kwdef struct G id::Int @@ -114,7 +114,7 @@ Base.@kwdef struct G f::F end -Structs.kwdef(::Type{G}) = true +StructUtils.kwdef(::Type{G}) = true struct H id::Int @@ -165,7 +165,7 @@ Base.@kwdef struct System shell::Union{Nothing, Dict} = nothing end -Structs.kwdef(::Type{System}) = true +StructUtils.kwdef(::Type{System}) = true struct L id::Int