Skip to content

Commit 7697a0e

Browse files
committed
add Tests
1 parent 9ae065f commit 7697a0e

File tree

6 files changed

+219
-4
lines changed

6 files changed

+219
-4
lines changed

Project.toml

+11-2
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,20 @@ uuid = "d3baf920-4533-4796-be79-acb40839d791"
33
authors = ["Jun Tian <[email protected]> and contributors"]
44
version = "0.1.0"
55

6-
[extras]
7-
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
6+
[deps]
7+
JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1"
8+
TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53"
9+
URIs = "5c2747f8-b7ea-4ff2-ba2e-563bfd36b1d4"
10+
UUIDs = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
811

912
[compat]
1013
julia = "1"
14+
JSON3 = "1"
15+
TimeZones = "1"
16+
URIs = "1"
17+
18+
[extras]
19+
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
1120

1221
[targets]
1322
test = ["Test"]

README.md

+28
Original file line numberDiff line numberDiff line change
@@ -1 +1,29 @@
11
# CloudEvents
2+
3+
This package provides the Julia SDK for [CloudEvents](https://github.com/cloudevents/spec).
4+
5+
## Basic Usage
6+
7+
Below we provide the most common usages.
8+
9+
```julia
10+
using CloudEvents
11+
12+
# Create a CloudEvent manually
13+
data = Dict("message" => "Hello World!")
14+
ce = CloudEvent(data; type="example", source="https://example.com/event-producer")
15+
16+
headers, body = to_http(ce) # structure mode by default
17+
# headers, body = to_http(ce, :binary) # or binary mode
18+
19+
# Send CloudEvent
20+
using HTTP
21+
22+
HTTP.post("<your url>", headers, body)
23+
24+
# Receive CloudEvent
25+
using JSON3
26+
HTTP.serve() do req
27+
ce = from_http(HTTP.headers(req), JSON3.read(req.body))
28+
end
29+
```

src/CloudEvents.jl

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module CloudEvents
22

3-
# Write your package code here.
3+
include("cloud_event.jl")
4+
include("http.jl")
45

56
end

src/cloud_event.jl

+107
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
export CloudEvent
2+
3+
using URIs: URI
4+
using TimeZones: ZonedDateTime, localzone, now
5+
using UUIDs: uuid4
6+
7+
mutable struct CloudEvent{T}
8+
# required
9+
id::String
10+
source::URI
11+
specversion::String
12+
type::String
13+
14+
# optional
15+
datacontenttype::Union{String,Nothing}
16+
dataschema::Union{URI,Nothing}
17+
subject::Union{String,Nothing}
18+
time::Union{ZonedDateTime,Nothing}
19+
20+
# extra
21+
extensions::Dict
22+
23+
data::T
24+
25+
function CloudEvent(id, source, specversion, type, datacontenttype, dataschema, subject, time, extensions, data)
26+
if source isa String
27+
source = URI(source)
28+
end
29+
if time isa String
30+
time = ZonedDateTime(time, "yyyy-mm-ddTHH:MM:SSz")
31+
end
32+
if dataschema isa String
33+
dataschema = URI(dataschema)
34+
end
35+
36+
specversion == "0.3" || specversion == "1.0" || @error "unknown specversion $specversion"
37+
38+
new{typeof(data)}(id, source, specversion, type, datacontenttype, dataschema, subject, time, extensions, data)
39+
end
40+
end
41+
42+
required_fields(::Type{<:CloudEvent}) = ("id", "source", "specversion", "type")
43+
optional_fields(::Type{<:CloudEvent}) = ("datacontenttype", "dataschema", "subject", "time")
44+
known_fields(ce::Type{<:CloudEvent}) = (required_fields(ce)..., optional_fields(ce)...)
45+
46+
function CloudEvent(
47+
data
48+
; type,
49+
source,
50+
id=string(uuid4()),
51+
time=now(localzone()),
52+
specversion="1.0",
53+
datacontenttype=nothing,
54+
dataschema=nothing,
55+
subject=nothing,
56+
kw...
57+
)
58+
extensions = Dict(convert(String, k) => v for (k, v) in kw)
59+
CloudEvent(id, source, specversion, type, datacontenttype, dataschema, subject, time, extensions, data)
60+
end
61+
62+
Base.getindex(ce::CloudEvent) = ce.data
63+
64+
function Base.getindex(ce::CloudEvent, k::String)
65+
if k in known_fields(typeof(ce))
66+
getfield(ce, Symbol(k))
67+
else
68+
get(ce.extensions, k, nothing)
69+
end
70+
end
71+
72+
Base.setindex!(ce::CloudEvent, v) = ce.data = v
73+
74+
function Base.setindex!(ce::CloudEvent, v, k)
75+
if k in known_fields(typeof(ce))
76+
setfield!(ce, Symbol(k), v)
77+
else
78+
setindex!(ce.extensions, v, k)
79+
end
80+
end
81+
82+
Base.keys(ce::CloudEvent) = (known_fields(typeof(ce))..., keys(ce.extensions)...)
83+
84+
function Base.convert(::Type{Dict}, ce::CloudEvent)
85+
res = Dict(
86+
"id" => ce.id,
87+
"source" => ce.resource,
88+
"specversion" => ce.specversion,
89+
"type" => ce.type,
90+
"datacontenttype" => ce.datacontenttype,
91+
"dataschema" => ce.dataschema,
92+
"subject" => ce.subject,
93+
"time" => ce.time
94+
;
95+
ce.extensions...
96+
)
97+
98+
res["data"] = ce.data
99+
res
100+
end
101+
102+
Base.convert(T::Type{<:CloudEvent}, x::AbstractDict) = CloudEvent(
103+
(x[f] for f in required_fields(T))...,
104+
(get(x, f, nothing) for f in optional_fields(T))...,
105+
Dict(f => x[f] for f in keys(x) if x known_fields(T)),
106+
get(x, "data", nothing)
107+
)

src/http.jl

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
export from_http, to_http
2+
3+
using JSON3
4+
5+
function from_http(headers, body)
6+
content_type = "application/cloudevents+json"
7+
8+
ce = Dict()
9+
for (k, v) in headers
10+
k = lowercase(k)
11+
if k == "content-type"
12+
content_type = v
13+
elseif startswith(k, "ce-")
14+
ce[k[4:end]] = v
15+
end
16+
end
17+
ce["data"] = body
18+
19+
# FIXME: `application/json; charset=utf-8` is not handled
20+
if content_type == "application/cloudevents+json"
21+
ce = Dict(string(k) => v for (k, v) in pairs(JSON3.read(body)))
22+
end
23+
24+
convert(CloudEvent, ce)
25+
end
26+
27+
to_http(ce::CloudEvent, mode::Symbol=:structure) = to_http(ce, Val(mode))
28+
29+
# https://github.com/cloudevents/spec/blob/main/cloudevents/bindings/http-protocol-binding.md#31-binary-content-mode
30+
function to_http(ce::CloudEvent, ::Val{:binary})
31+
headers = ["ce-$k" => string(ce[k]) for k in keys(ce) if !isnothing(ce[k])]
32+
body = ce[]
33+
headers, body
34+
end
35+
36+
# https://github.com/cloudevents/spec/blob/main/cloudevents/bindings/http-protocol-binding.md#32-structured-content-mode
37+
function to_http(ce::CloudEvent, ::Val{:structure})
38+
headers = ["Content-Type" => "application/cloudevents+json"]
39+
body = JSON3.write(Dict(k => string(ce[k]) for k in keys(ce) if !isnothing(ce[k]))) # TODO: more efficient, allow custom serialization
40+
headers, body
41+
end

test/runtests.jl

+30-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,35 @@
11
using CloudEvents
22
using Test
3+
using JSON3
34

45
@testset "CloudEvents.jl" begin
5-
# Write your tests here.
6+
7+
@testset "uppercase header" begin
8+
headers = [
9+
"Ce-Id" => "my-id",
10+
"Ce-Source" => "<event-source>",
11+
"Ce-Type" => "cloudevent.event.type",
12+
"Ce-Specversion" => "1.0",
13+
"Content-Type" => "text/plain"
14+
]
15+
16+
ce = from_http(headers, nothing)
17+
18+
@test isnothing(ce[])
19+
end
20+
21+
@testset "cloudevents+json" begin
22+
headers = ["Content-Type" => "application/cloudevents+json"]
23+
body = JSON3.write(Dict(
24+
"specversion" => "1.0",
25+
"source" => "s",
26+
"type" => "t",
27+
"id" => "1234-1234-1234",
28+
"data" => Dict("foo" => "bar"),
29+
"extra_baz" => "baz"
30+
))
31+
ce = from_http(headers, body)
32+
@test ce["extra_baz"] == "baz"
33+
headers, body = to_http(ce)
34+
end
635
end

0 commit comments

Comments
 (0)