diff --git a/Project.toml b/Project.toml index ee91d7d..a130e27 100644 --- a/Project.toml +++ b/Project.toml @@ -7,7 +7,14 @@ version = "0.1.12" Accessors = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" +[weakdeps] +DimensionalData = "0703355e-b756-11e9-17c0-8b28908087d0" + +[extensions] +SpaceDataModelDimensionalDataExt = "DimensionalData" + [compat] Accessors = "0.1" Dates = "1" +DimensionalData = "0.29" julia = "1.10" diff --git a/docs/src/interface.md b/docs/src/interface.md index 132cd17..3a2974e 100644 --- a/docs/src/interface.md +++ b/docs/src/interface.md @@ -6,6 +6,22 @@ SpaceDataModel.jl provides a set of abstractions and generic functions that can - [DataAPI.jl](https://github.com/JuliaData/DataAPI.jl): A data-focused namespace for packages to share functions +## Metadata + +The package exports the following functions for metadata handling: + +- [`getmeta`](@ref): Get metadata for an object +- [`setmeta`](@ref): Update metadata for an object +- [`setmeta!`](@ref): Update metadata for an object in-place + +These functions are generic and can be extended to support custom data types. + +```@docs; canonical=false +getmeta +setmeta +setmeta! +``` + ## Coordinate Systems The package exports three key components for coordinate system handling: diff --git a/ext/SpaceDataModelDimensionalDataExt.jl b/ext/SpaceDataModelDimensionalDataExt.jl new file mode 100644 index 0000000..24d0b52 --- /dev/null +++ b/ext/SpaceDataModelDimensionalDataExt.jl @@ -0,0 +1,7 @@ +module SpaceDataModelDimensionalDataExt +using DimensionalData: AbstractDimArray, NoMetadata, metadata +import SpaceDataModel: meta, _merge + +_merge(::NoMetadata, d, rest...) = merge(d, rest...) +meta(A::AbstractDimArray) = metadata(A) +end diff --git a/src/SpaceDataModel.jl b/src/SpaceDataModel.jl index c4cbada..431dbd5 100644 --- a/src/SpaceDataModel.jl +++ b/src/SpaceDataModel.jl @@ -10,6 +10,7 @@ export AbstractDataVariable export Project, Instrument, DataSet, LDataSet, Product export Event export AbstractCoordinateSystem, AbstractCoordinateVector, getcsys +export getmeta, setmeta!, setmeta include("utils.jl") include("metadata.jl") @@ -25,8 +26,25 @@ include("coord.jl") include("workload.jl") # Interface -name(v) = @getfield v :name get(meta(v), "name", "") -meta(v) = @getfield v (:meta, :metadata) NoMetadata() +name(v) = @getfield v :name getmeta(v, "name", "") + +""" + getmeta(x) + +Get metadata for object `x`. If `x` does not have metadata, return `NoMetadata()`. + +""" +getmeta(x) = @getfield x (:meta, :metadata) NoMetadata() + +""" + getmeta(x, key, default=nothing) + +Get metadata value associated with object `x` for key `key`, or `default` if `key` is not present. +""" +getmeta(x, key, default=nothing) = get(meta(x), key, default) + +meta(x) = getmeta(x) # not exported (to be removed) + units(v) = @get(v, "units", nothing) times(v) = @getfield v (:times, :time) @@ -35,4 +53,43 @@ function unit(v) allequal(us) ? only(us) : error("Units are not equal: $us") end +""" + setmeta!(x, key => value, ...; symbolkey => value2, ...) + setmeta!(x, dict::AbstractDict) + +Update metadata for object `x` in-place and return `x`. The metadata container must be mutable. + +The arguments could be multiple key-value pairs or a dictionary of metadata; keyword arguments are also accepted. + +# Examples +```julia +setmeta!(x, :units => "m/s", :source => "sensor") +setmeta!(x, Dict(:units => "m/s", :quality => "good")) +setmeta!(x; units="m/s", calibrated=true) +``` + +Throws an error if the metadata is not mutable. Use `setmeta` for immutable metadata. +""" +function setmeta! end + +function setmeta!(x, args...; kw...) + m = meta(x) + ismutable(m) || error("Metadata is not mutable, use `setmeta` instead") + set!(m, args...; kw...) + return x +end + +""" + setmeta(x, key => value, ...; symbolkey => value2, ...) + setmeta(x, dict::AbstractDict) + +Update metadata for object `x` for key `key` to have value `value` and return `x`. +""" +function setmeta end + +function setmeta(x, args::Pair...; kw...) + return @set x.metadata=_merge(meta(x), Dict(args...), kw) end +setmeta(x, dict::AbstractDict) = @set x.metadata=_merge(meta(x), dict) + +end \ No newline at end of file diff --git a/src/metadata.jl b/src/metadata.jl index c240baf..88f4706 100644 --- a/src/metadata.jl +++ b/src/metadata.jl @@ -17,9 +17,10 @@ Base.values(::NoMetadata) = () Base.iterate(::NoMetadata) = nothing # Allow merging NoMetadata with a Dict or keyword arguments -Base.merge(::NoMetadata, d::AbstractDict) = length(d) == 0 ? NoMetadata() : copy(d) -Base.merge(::NoMetadata, p::Base.Pairs) = length(p) == 0 ? NoMetadata() : p -Base.merge(m::NoMetadata, d, rest...) = merge(merge(m, d), rest...) +function Base.merge(::NoMetadata, d, rest...) + res = merge(d, rest...) + isempty(res) ? NoMetadata() : res # for cases where no kwarg is provided +end Base.haskey(::NoMetadata, args...) = false Base.get(::NoMetadata, key, default=nothing) = default diff --git a/src/utils.jl b/src/utils.jl index 9b7293b..8e7bd46 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -86,3 +86,14 @@ function colors(i) end print_name(io::IO, var) = printstyled(io, name(var); color=colors(7)) + +# like merge to avoid privacy issues +_merge(a, b...) = merge(a, b...) + +function set!(d::AbstractDict, args::Pair...; kw...) + for (k, v) in args + d[k] = v + end + return merge!(d, kw) +end +set!(d::AbstractDict, dict::AbstractDict; kw...) = merge!(d, dict, kw) diff --git a/test/DimensionalData.jl b/test/DimensionalData.jl new file mode 100644 index 0000000..d151e8b --- /dev/null +++ b/test/DimensionalData.jl @@ -0,0 +1,19 @@ +@testitem "DimensionalData Extension Tests" begin + using DimensionalData + # Create test data + A = DimArray(rand(10, 5), (X(1:10), Y(1:5))) + A_mutable = rebuild(A, metadata=Dict()) + + # Test with key-value pairs and keyword arguments + @test_nowarn setmeta(A, "string" => "supported", description="velocity", calibrated=true) + @test_throws ErrorException setmeta!(A, "string" => "supported", calibrated=true) + @test_nowarn setmeta!(A_mutable, "string" => "supported", calibrated=true) + @test A_mutable.metadata == Dict("string" => "supported", :calibrated => true) + + # Test with Dict + meta_dict = Dict("string" => "supported", :calibrated => false) + @test_nowarn setmeta(A, meta_dict) + @test_nowarn setmeta(A_mutable, meta_dict) + @test_nowarn setmeta!(A_mutable, meta_dict) + @test A_mutable.metadata == meta_dict +end diff --git a/test/Project.toml b/test/Project.toml index 08a6e2b..67eb1ac 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,6 +1,7 @@ [deps] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" CheckConcreteStructs = "73c92db5-9da6-4938-911a-6443a7e94a58" +DimensionalData = "0703355e-b756-11e9-17c0-8b28908087d0" JET = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" diff --git a/test/product.jl b/test/product.jl index 98bcc99..ba96332 100644 --- a/test/product.jl +++ b/test/product.jl @@ -23,7 +23,7 @@ # Test construction with kwargs that become metadata p5 = Product([1, 2, 3]; units="m/s", description="velocity") - @test p5.metadata isa Base.Pairs + @test p5.metadata isa AbstractDict @test p5.metadata[:units] == "m/s" @test p5.metadata[:description] == "velocity"