Skip to content
Closed
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
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ This package provides types and helper functions for dealing with the HTTP proto
## Documentation
### Request

A `Request` represents an HTTP request sent by a client to a server.
A `Request` represents an HTTP request sent by a client to a server.

:::julia
type Request
Expand All @@ -31,7 +31,7 @@ A `Request` represents an HTTP request sent by a client to a server.

* `method` is an HTTP methods string ("GET", "PUT", etc)
* `resource` is the url resource requested ("/hello/world")
* `headers` is a `Dict` of field name `String`s to value `String`s
* `headers` is an `Associative` of field name `String`s to value `String`s
* `data` is the data in the request

### Response
Expand All @@ -47,7 +47,7 @@ A `Response` represents an HTTP response sent to a client by a server.
end

* `status` is the HTTP status code (see `STATUS_CODES`) [default: `200`]
* `headers` is the `Dict` of headers [default: `headers()`, see Headers below]
* `headers` is the headers [default: `headers()`, see Headers below]
* `data` is the response data (as a `String` or `Array{Uint8}`) [default: `""`]
* `finished` is `true` if the `Reponse` is valid, meaning that it can be converted to an actual HTTP response [default: `false`]

Expand All @@ -61,7 +61,11 @@ There are a variety of constructors for `Response`, which set sane defaults for

### Headers

`Headers` is a type alias for `Dict{String,String}`.
`Headers` is an `Associative{String,String}`. You can interact with it
much like a Dict, but it can have multiple keys with the same
value. For example `Set-Cookie` may be set multiple time for multiple
cookies.

There is a default constructor, `headers`, to produce a reasonable default set of headers.
The defaults are as follows:

Expand Down
84 changes: 77 additions & 7 deletions src/HttpCommon.jl
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,12 @@ export STATUS_CODES,
decodeURI,
parsequerystring,
FileResponse,
mimetypes
mimetypes,
headersforkey

include("mimetypes.jl")

import Base.show
import Base: done, next, start, get, setindex!, delete!, show, length, convert

const STATUS_CODES = Dict([
(100, "Continue"),
Expand Down Expand Up @@ -129,16 +130,85 @@ RFC1123_datetime() = RFC1123_datetime(Dates.now(Dates.UTC))

# HTTP Headers
#
# Dict Type for HTTP headers
# Type for HTTP headers
# `headers()` for building default Response Headers
#
typealias Headers Dict{String,String}
headers() = Dict{String,String}([
type Headers <: Associative{String, String}
data::Dict{String,Vector{String}}
length::Int
end
Headers() = Headers(Dict{String,Vector{String}}(), 0)
Headers{K<:String,V<:String}(d::Associative{K,V}) = merge!(Headers(), d)
headers() = Headers(Dict{String,String}([
("Server" , "Julia/$VERSION"),
("Content-Type" , "text/html; charset=utf-8"),
("Content-Language" , "en"),
("Date" , RFC1123_datetime())
])
]))

convert{K<:String,V<:String}(::Type{Headers}, d::Associative{K,V}) = Headers(d)

function start(h::Headers)
vals = (String)[]
(start(h.data), nothing, vals, start(vals))
end

function done(h::Headers, s)
dictst, key, vals, valsst = s
done(vals, valsst) && done(h.data, dictst)
end

function next(h::Headers, s)
dictst, key, vals, valsst = s
while done(vals, valsst)
((key, vals), dictst) = next(h.data, dictst)
valsst = start(vals)
end
val, valsst = next(vals, valsst)
((key, val), (dictst, key, vals, valsst))
end

function show(io::IO, h::Headers)
println(io, "Headers :")
for (k,v) in h
println(io, " $k: $v")
end
end

function get(h::Headers, k::String, default)
vs = get(h.data, k, nothing)
if is(vs, nothing)
return default
end
@assert length(vs) != 0
if length(vs) > 1
warn("getindex() called for header \"$k\" with multiple values")
end
vs[1]
end

function setindex!(h::Headers, v::String, k::String)
h.length += 1
if haskey(h.data, k)
push!(h.data[k], v)
else
h.data[k] = [v]
end
return h
end

function delete!(h::Headers, k::String)
vs = get(h.data, k, nothing)
if !is(vs, nothing)
h.length -= length(vs)
end
delete!(h.data, k)
h
end

length(h::Headers) = h.length

headersforkey(h::Headers, k::String) = h.data[k]

# HTTP request
#
Expand Down Expand Up @@ -198,7 +268,7 @@ function FileResponse(filename)
Response(200, Dict{String,String}([("Content-Type",mime)]), s)
else
Response(404, "Not Found - file $filename could not be found")
end
end
end

# Escape HTML characters
Expand Down
30 changes: 28 additions & 2 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,34 @@ facts("HttpCommon utility functions") do
@fact parsequerystring("foo=%3Ca%20href%3D%27foo%27%3Ebar%3C%2Fa%3Erun%26%2B%2B&bar=123") =>
["foo" => "<a href='foo'>bar</a>run&++", "bar" => "123"]
end

end

# Check doesn't throw
RFC1123_datetime()
RFC1123_datetime()

facts("Headers") do
context("Headers with duplicates") do
h = Headers()
@fact length(h) => 0

h["Content-Type"] = "text/html"
@fact length(h) => 1

h["Set-Cookie"] = "user=me;"
@fact length(h) => 2

h["Set-Cookie"] = "pass=secret;"
@fact length(h) => 3

@fact h["Content-Type"] => "text/html"
@fact headersforkey(h, "Set-Cookie") => ["user=me;", "pass=secret;"]
@fact h |> collect |> sort => [("Content-Type", "text/html"),
("Set-Cookie", "pass=secret;"),
("Set-Cookie", "user=me;")]

delete!(h, "Set-Cookie")
@fact length(h) => 1
@fact h["Content-Type"] => "text/html"
@fact get(h, "Set-Cookie", "**DEFAULT**") => "**DEFAULT**"
end
end