Skip to content

Commit

Permalink
feature/protobuf-extension (#172)
Browse files Browse the repository at this point in the history
* added protobuf extension and tests
* added docs for the ProtoBuf.jl extention
  • Loading branch information
ndortega authored Mar 14, 2024
1 parent 2d4a12f commit cd1a4dc
Show file tree
Hide file tree
Showing 16 changed files with 426 additions and 10 deletions.
4 changes: 3 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Pkg = "^1"
Reexport = "^1"
RelocatableFolders = "^1"
Requires = "^1"
ProtoBuf = "^1"
Sockets = "^1"
Statistics = "^1"
StructTypes = "^1"
Expand All @@ -43,10 +44,11 @@ CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
Mustache = "ffc61752-8dc7-55ee-8c37-f3e9cdd09e70"
OteraEngine = "b2d7f28f-acd6-4007-8b26-bc27716e5513"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
ProtoBuf = "3349acd9-ac6a-5e09-bcdb-63829b23a429"
StructTypes = "856f2bd8-1eba-4b0a-8007-ebc267875bd4"
Suppressor = "fd094767-a336-5f1f-9728-57cf17d0bbfb"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
WGLMakie = "276b4fcb-3e11-5398-bf8b-a0c2d153d008"

[targets]
test = ["Bonito", "CairoMakie", "Mustache", "OteraEngine", "Pkg", "StructTypes", "Test", "Suppressor", "WGLMakie"]
test = ["Bonito", "CairoMakie", "Mustache", "OteraEngine", "Pkg", "ProtoBuf", "StructTypes", "Suppressor", "Test", "WGLMakie"]
58 changes: 57 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Breathe easy knowing you can quickly spin up a web server with abstractions you'
- Static & Dynamic file hosting
- Templating Support
- Plotting Support
- Protocol Buffer Support
- Route tagging
- Repeat tasks

Expand Down Expand Up @@ -537,7 +538,62 @@ end
# start the web server in parallel mode
serveparallel()
```
## Plotting Support

## Protocol Buffers

Oxygen includes an extension for the [ProtoBuf.jl](https://github.com/JuliaIO/ProtoBuf.jl) package. This extension provides a `protobuf()` function, simplifying the process of working with Protocol Buffers in the context of web server. For a better understanding of this package, please refer to its official documentation.



This function has overloads for the following scenarios:
- Decoding a protobuf message from the body of an HTTP request.
- Encoding a protobuf message into the body of an HTTP request.
- Encoding a protobuf message into the body of an HTTP response.


```julia
using HTTP
using ProtoBuf
using Oxygen

# The generated classes need to be created ahead of time (check the protobufs)
include("people_pb.jl");
using .people_pb: People, Person

# Decode a Protocol Buffer Message
@post "/count" function(req::HTTP.Request)
# decode the request body into a People object
message = protobuf(req, People)
# count the number of Person objects
return length(message.people)
end

# Encode & Return Protocol Buffer message
@get "/get" function()
message = People([
Person("John Doe", 20),
Person("Alice", 30),
Person("Bob", 35)
])
# seralize the object inside the body of a HTTP.Response
return protobuf(message)
end
```

The following is an example of a schema that was used to create the necessary Julia bindings. These bindings allow for the encoding and decoding of messages in the above example.
```protobuf
syntax = "proto3";
message Person {
string name = 1;
sint32 age = 2;
}
message People {
repeated Person people = 1;
}
```


## Plotting

Oxygen is equipped with several package extensions that enhance its plotting capabilities. These extensions make it easy to return plots directly from request handlers. All operations are performed in-memory using an IOBuffer and return a `HTTP.Response`

Expand Down
58 changes: 57 additions & 1 deletion docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Breathe easy knowing you can quickly spin up a web server with abstractions you'
- Static & Dynamic file hosting
- Templating Support
- Plotting Support
- Protocol Buffer Support
- Route tagging
- Repeat tasks

Expand Down Expand Up @@ -537,7 +538,62 @@ end
# start the web server in parallel mode
serveparallel()
```
## Plotting Support

## Protocol Buffers

Oxygen includes an extension for the [ProtoBuf.jl](https://github.com/JuliaIO/ProtoBuf.jl) package. This extension provides a `protobuf()` function, simplifying the process of working with Protocol Buffers in the context of web server. For a better understanding of this package, please refer to its official documentation.



This function has overloads for the following scenarios:
- Decoding a protobuf message from the body of an HTTP request.
- Encoding a protobuf message into the body of an HTTP request.
- Encoding a protobuf message into the body of an HTTP response.


```julia
using HTTP
using ProtoBuf
using Oxygen

# The generated classes need to be created ahead of time (check the protobufs)
include("people_pb.jl");
using .people_pb: People, Person

# Decode a Protocol Buffer Message
@post "/count" function(req::HTTP.Request)
# decode the request body into a People object
message = protobuf(req, People)
# count the number of Person objects
return length(message.people)
end

# Encode & Return Protocol Buffer message
@get "/get" function()
message = People([
Person("John Doe", 20),
Person("Alice", 30),
Person("Bob", 35)
])
# seralize the object inside the body of a HTTP.Response
return protobuf(message)
end
```

The following is an example of a schema that was used to create the necessary Julia bindings. These bindings allow for the encoding and decoding of messages in the above example.
```protobuf
syntax = "proto3";
message Person {
string name = 1;
sint32 age = 2;
}
message People {
repeated Person people = 1;
}
```


## Plotting

Oxygen is equipped with several package extensions that enhance its plotting capabilities. These extensions make it easy to return plots directly from request handlers. All operations are performed in-memory using an IOBuffer and return a `HTTP.Response`

Expand Down
8 changes: 7 additions & 1 deletion src/extensions/load.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ end

function __init__()


################################################################
# Serialization Extensions #
################################################################
@require ProtoBuf = "3349acd9-ac6a-5e09-bcdb-63829b23a429" include("serialization/protobuf.jl")


################################################################
# Templating Extensions #
################################################################
Expand All @@ -42,6 +49,5 @@ function __init__()
include("plotting/wglmakie.jl")
end
end


end
73 changes: 73 additions & 0 deletions src/extensions/serialization/protobuf.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import HTTP
import .ProtoBuf: encode, decode, ProtoDecoder, ProtoEncoder

export protobuf

"""
protobuf(request::HTTP.Request, type::Type{T}) :: T where {T}
Decode a protobuf message from the body of an HTTP request.
# Arguments
- `request`: An HTTP request object containing the protobuf message in its body.
- `type`: The type of the protobuf message to decode.
# Returns
- The decoded protobuf message of the specified type.
"""
function protobuf(request::HTTP.Request, type::Type{T}) :: T where {T}
io = IOBuffer(request.body)
return decode(ProtoDecoder(io), type)
end


"""
protobuf(content::T, url::String, method::String = "POST") :: HTTP.Request where {T}
Encode a protobuf message into the body of an HTTP.Request
# Arguments
- `content`: The protobuf message to encode.
- `target`: The target (URL) to which the request will be sent.
- `method`: The HTTP method for the request (default is "POST").
- `headers`: The HTTP headers for the request (default is an empty list).
# Returns
- An HTTP request object with the encoded protobuf message in its body.
"""
function protobuf(content::T, target::String; method = "POST", headers = []) :: HTTP.Request where {T}
io = IOBuffer()
encode(ProtoEncoder(io), content)
body = take!(io)
# Format the request
request = HTTP.Request(method, target, headers, body)
HTTP.setheader(request, "Content-Type" => "application/octet-stream")
HTTP.setheader(request, "Content-Length" => string(sizeof(body)))
return request
end


"""
protobuf(content::T; status = 200, headers = []) :: HTTP.Response where {T}
Encode a protobuf message into the body of an HTTP.Response
# Arguments
- `content`: The protobuf message to encode.
- `status`: The HTTP status code for the response (default is 200).
- `headers`: The HTTP headers for the response (default is an empty list).
# Returns
- An HTTP response object with the encoded protobuf message in its body.
"""
function protobuf(content::T; status = 200, headers = []) :: HTTP.Response where {T}
io = IOBuffer()
encode(ProtoEncoder(io), content)
body = take!(io)
# Format the response
response = HTTP.Response(status, headers, body = body)
HTTP.setheader(response, "Content-Type" => "application/octet-stream")
HTTP.setheader(response, "Content-Length" => string(sizeof(body)))
return response
end

4 changes: 2 additions & 2 deletions src/utilities/bodyparsers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ end
Read the body of a HTTP.Request as JSON with additional arguments for the read/serializer into a custom struct.
"""
function json(req::HTTP.Request, classtype; kwargs...)
function json(req::HTTP.Request, classtype::Type{T}; kwargs...) :: T where {T}
body = IOBuffer(HTTP.payload(req))
return eof(body) ? nothing : JSON3.read(body, classtype; kwargs...)
end
Expand Down Expand Up @@ -96,6 +96,6 @@ end
Read the body of a HTTP.Messages.Response as JSON with additional keyword arguments and serialize it into a custom struct
"""
function json(response::HTTP.Messages.Response, classtype; kwargs...)
function json(response::HTTP.Messages.Response, classtype::Type{T}; kwargs...) :: T where {T}
return JSON3.read(String(response.body), classtype; kwargs...)
end
File renamed without changes.
File renamed without changes.
10 changes: 10 additions & 0 deletions test/extensions/protobuf/messages/people.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
syntax = "proto3";

message Person {
string name = 1;
sint32 age = 2;
}

message People {
repeated Person people = 1;
}
77 changes: 77 additions & 0 deletions test/extensions/protobuf/messages/people_pb.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Autogenerated using ProtoBuf.jl v1.0.15 on 2024-03-10T22:46:41.410
# original file: D:\Programming\JuliaProjects\oxygen-demo\src\people.proto (proto3 syntax)

module people_pb

import ProtoBuf as PB
using ProtoBuf: OneOf
using ProtoBuf.EnumX: @enumx

export Person, People

struct Person
name::String
age::Int32
end
PB.default_values(::Type{Person}) = (;name = "", age = zero(Int32))
PB.field_numbers(::Type{Person}) = (;name = 1, age = 2)

function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:Person})
name = ""
age = zero(Int32)
while !PB.message_done(d)
field_number, wire_type = PB.decode_tag(d)
if field_number == 1
name = PB.decode(d, String)
elseif field_number == 2
age = PB.decode(d, Int32, Val{:zigzag})
else
PB.skip(d, wire_type)
end
end
return Person(name, age)
end

function PB.encode(e::PB.AbstractProtoEncoder, x::Person)
initpos = position(e.io)
!isempty(x.name) && PB.encode(e, 1, x.name)
x.age != zero(Int32) && PB.encode(e, 2, x.age, Val{:zigzag})
return position(e.io) - initpos
end
function PB._encoded_size(x::Person)
encoded_size = 0
!isempty(x.name) && (encoded_size += PB._encoded_size(x.name, 1))
x.age != zero(Int32) && (encoded_size += PB._encoded_size(x.age, 2, Val{:zigzag}))
return encoded_size
end

struct People
people::Vector{Person}
end
PB.default_values(::Type{People}) = (;people = Vector{Person}())
PB.field_numbers(::Type{People}) = (;people = 1)

function PB.decode(d::PB.AbstractProtoDecoder, ::Type{<:People})
people = PB.BufferedVector{Person}()
while !PB.message_done(d)
field_number, wire_type = PB.decode_tag(d)
if field_number == 1
PB.decode!(d, people)
else
PB.skip(d, wire_type)
end
end
return People(people[])
end

function PB.encode(e::PB.AbstractProtoEncoder, x::People)
initpos = position(e.io)
!isempty(x.people) && PB.encode(e, 1, x.people)
return position(e.io) - initpos
end
function PB._encoded_size(x::People)
encoded_size = 0
!isempty(x.people) && (encoded_size += PB._encoded_size(x.people, 1))
return encoded_size
end
end # module
6 changes: 6 additions & 0 deletions test/extensions/protobuf/messages/test.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
syntax = "proto3";

message MyMessage {
sint32 a = 1;
repeated string b = 2;
}
Loading

0 comments on commit cd1a4dc

Please sign in to comment.