Skip to content

Commit

Permalink
Allow mutation in server-side service implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
bgreni committed Nov 30, 2024
1 parent 61a2f8b commit a108f32
Show file tree
Hide file tree
Showing 14 changed files with 67 additions and 45 deletions.
32 changes: 26 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,44 +51,57 @@ Learn how to get up and running with Mojo on the [Modular website](https://www.m
Once you have a Mojo project set up locally,

1. Add the `mojo-community` channel to your `mojoproject.toml`, e.g:

```toml
[project]
channels = ["conda-forge", "https://conda.modular.com/max", "https://repo.prefix.dev/mojo-community"]
```

2. Add `lightbug_http` as a dependency:

```toml
[dependencies]
lightbug_http = ">=0.1.5"
```

3. Run `magic install` at the root of your project, where `mojoproject.toml` is located
4. Lightbug should now be installed as a dependency. You can import all the default imports at once, e.g:

```mojo
from lightbug_http import *
```
or import individual structs and functions, e.g.
```mojo
from lightbug_http.service import HTTPService
from lightbug_http.http import HTTPRequest, HTTPResponse, OK, NotFound
```
there are some default handlers you can play with:
```mojo
from lightbug_http.service import Printer # prints request details to console
from lightbug_http.service import Welcome # serves an HTML file with an image (currently requires manually adding files to static folder, details below)
from lightbug_http.service import ExampleRouter # serves /, /first, /second, and /echo routes
```
5. Add your handler in `lightbug.🔥` by passing a struct that satisfies the following trait:
```mojo
trait HTTPService:
fn func(self, req: HTTPRequest) raises -> HTTPResponse:
fn func(inout self, req: HTTPRequest) raises -> HTTPResponse:
...
```

For example, to make a `Printer` service that prints some details about the request to console:

```mojo
from lightbug_http import *
@value
struct Printer(HTTPService):
fn func(self, req: HTTPRequest) raises -> HTTPResponse:
fn func(inout self, req: HTTPRequest) raises -> HTTPResponse:
var uri = req.uri
print("Request URI: ", to_string(uri.request_uri))
Expand All @@ -104,7 +117,9 @@ Once you have a Mojo project set up locally,
return OK(body)
```
6. Start a server listening on a port with your service like so.

6. Start a server listening on a port with your service like so.

```mojo
from lightbug_http import Welcome, Server
Expand All @@ -113,20 +128,22 @@ Once you have a Mojo project set up locally,
var handler = Welcome()
server.listen_and_serve("0.0.0.0:8080", handler)
```
Feel free to change the settings in `listen_and_serve()` to serve on a particular host and port.
Now send a request `0.0.0.0:8080`. You should see some details about the request printed out to the console.
Congrats 🥳 You're using Lightbug!
Routing is not in scope for this library, but you can easily set up routes yourself:
```mojo
from lightbug_http import *
@value
struct ExampleRouter(HTTPService):
fn func(self, req: HTTPRequest) raises -> HTTPResponse:
fn func(inout self, req: HTTPRequest) raises -> HTTPResponse:
var body = req.body_raw
var uri = req.uri
Expand Down Expand Up @@ -156,7 +173,7 @@ from lightbug_http import *
@value
struct Welcome(HTTPService):
fn func(self, req: HTTPRequest) raises -> HTTPResponse:
fn func(inout self, req: HTTPRequest) raises -> HTTPResponse:
var uri = req.uri
if uri.path == "/":
Expand Down Expand Up @@ -216,10 +233,13 @@ fn main() -> None:
Pure Mojo-based client is available by default. This client is also used internally for testing the server.

## Switching between pure Mojo and Python implementations

By default, Lightbug uses the pure Mojo implementation for networking. To use Python's `socket` library instead, just import the `PythonServer` instead of the `Server` with the following line:

```mojo
from lightbug_http.python.server import PythonServer
```

You can then use all the regular server commands in the same way as with the default server.
Note: as of September, 2024, `PythonServer` and `PythonClient` throw a compilation error when starting. There's an open [issue](https://github.com/saviorand/lightbug_http/issues/41) to fix this - contributions welcome!

Expand Down
2 changes: 1 addition & 1 deletion lightbug_http/__init__.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ from lightbug_http.http import HTTPRequest, HTTPResponse, OK, NotFound, StatusCo
from lightbug_http.uri import URI
from lightbug_http.header import Header, Headers, HeaderKey
from lightbug_http.cookie import Cookie, RequestCookieJar, ResponseCookieJar
from lightbug_http.service import HTTPService, Welcome
from lightbug_http.service import HTTPService, Welcome, Counter
from lightbug_http.server import Server
from lightbug_http.strings import to_string

Expand Down
7 changes: 3 additions & 4 deletions lightbug_http/cookie/cookie.mojo
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from collections import Optional
from lightbug_http.header import HeaderKey


struct Cookie(CollectionElement):
alias EXPIRES = "Expires"
alias MAX_AGE = "Max-Age"
Expand All @@ -25,7 +26,6 @@ struct Cookie(CollectionElement):
var path: Optional[String]
var max_age: Optional[Duration]


@staticmethod
fn from_set_header(header_str: String) raises -> Self:
var parts = header_str.split(Cookie.SEPERATOR)
Expand Down Expand Up @@ -55,7 +55,7 @@ struct Cookie(CollectionElement):
elif part.startswith(Cookie.MAX_AGE):
cookie.max_age = Duration.from_string(part.removeprefix(Cookie.MAX_AGE + Cookie.EQUAL))
elif part.startswith(Cookie.EXPIRES):
var expires = Expiration.from_string(part.removeprefix(Cookie.EXPIRES + Cookie.EQUAL))
var expires = Expiration.from_string(part.removeprefix(Cookie.EXPIRES + Cookie.EQUAL))
if expires:
cookie.expires = expires.value()

Expand Down Expand Up @@ -86,7 +86,7 @@ struct Cookie(CollectionElement):
self.partitioned = partitioned

fn __str__(self) -> String:
return "Name: " + self.name + " Value: " + self.value
return "Name: " + self.name + " Value: " + self.value

fn __copyinit__(inout self: Cookie, existing: Cookie):
self.name = existing.name
Expand Down Expand Up @@ -120,7 +120,6 @@ struct Cookie(CollectionElement):
return Header(HeaderKey.SET_COOKIE, self.build_header_value())

fn build_header_value(self) -> String:

var header_value = self.name + Cookie.EQUAL + self.value
if self.expires.is_datetime():
var v = self.expires.http_date_timestamp()
Expand Down
10 changes: 2 additions & 8 deletions lightbug_http/cookie/duration.mojo
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
@value
struct Duration():
struct Duration:
var total_seconds: Int

fn __init__(
inout self,
seconds: Int = 0,
minutes: Int = 0,
hours: Int = 0,
days: Int = 0
):
fn __init__(inout self, seconds: Int = 0, minutes: Int = 0, hours: Int = 0, days: Int = 0):
self.total_seconds = seconds
self.total_seconds += minutes * 60
self.total_seconds += hours * 60 * 60
Expand Down
1 change: 1 addition & 0 deletions lightbug_http/cookie/expiration.mojo
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
alias HTTP_DATE_FORMAT = "ddd, DD MMM YYYY HH:mm:ss ZZZ"
alias TZ_GMT = TimeZone(0, "GMT")


@value
struct Expiration:
var variant: UInt8
Expand Down
2 changes: 1 addition & 1 deletion lightbug_http/cookie/request_cookie_jar.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ from lightbug_http.strings import to_string, lineBreak
from lightbug_http.header import HeaderKey, write_header
from lightbug_http.utils import ByteReader, ByteWriter, is_newline, is_space


@value
struct RequestCookieJar(Formattable, Stringable):
var _inner: Dict[String, String]
Expand Down Expand Up @@ -34,7 +35,6 @@ struct RequestCookieJar(Formattable, Stringable):
# TODO value must be "unquoted"
self._inner[key] = value


@always_inline
fn empty(self) -> Bool:
return len(self._inner) == 0
Expand Down
11 changes: 5 additions & 6 deletions lightbug_http/cookie/response_cookie_jar.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ struct ResponseCookieKey(KeyElement):
inout self,
name: String,
domain: Optional[String] = Optional[String](None),
path: Optional[String] = Optional[String](None)
path: Optional[String] = Optional[String](None),
):
self.name = name
self.domain = domain.or_else("")
Expand All @@ -39,6 +39,7 @@ struct ResponseCookieKey(KeyElement):
fn __hash__(self: Self) -> UInt:
return hash(self.name + "~" + self.domain + "~" + self.path)


@value
struct ResponseCookieJar(Formattable, Stringable):
var _inner: Dict[ResponseCookieKey, Cookie]
Expand Down Expand Up @@ -76,7 +77,7 @@ struct ResponseCookieJar(Formattable, Stringable):
return to_string(self)

fn __len__(self) -> Int:
return len(self._inner)
return len(self._inner)

@always_inline
fn set_cookie(inout self, cookie: Cookie):
Expand All @@ -86,7 +87,6 @@ struct ResponseCookieJar(Formattable, Stringable):
fn empty(self) -> Bool:
return len(self) == 0


fn from_headers(inout self, headers: List[String]) raises:
for header in headers:
try:
Expand All @@ -97,10 +97,9 @@ struct ResponseCookieJar(Formattable, Stringable):
fn encode_to(inout self, inout writer: ByteWriter):
for cookie in self._inner.values():
var v = cookie[].build_header_value()
write_header(writer, HeaderKey.SET_COOKIE , v)

write_header(writer, HeaderKey.SET_COOKIE, v)

fn format_to(self, inout writer: Formatter):
for cookie in self._inner.values():
var v = cookie[].build_header_value()
write_header(writer, HeaderKey.SET_COOKIE , v)
write_header(writer, HeaderKey.SET_COOKIE, v)
4 changes: 2 additions & 2 deletions lightbug_http/cookie/same_site.mojo
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@value
struct SameSite():
var value : UInt8
struct SameSite:
var value: UInt8

alias none = SameSite(0)
alias lax = SameSite(1)
Expand Down
3 changes: 1 addition & 2 deletions lightbug_http/http/request.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ struct HTTPRequest(Formattable, Stringable):
var protocol: String
var uri_str: String
try:
var rest = headers.parse_raw(reader)
var rest = headers.parse_raw(reader)
method, uri_str, protocol = rest[0], rest[1], rest[2]
except e:
raise Error("Failed to parse request headers: " + e.__str__())
Expand Down Expand Up @@ -89,7 +89,6 @@ struct HTTPRequest(Formattable, Stringable):
if HeaderKey.HOST not in self.headers:
self.headers[HeaderKey.HOST] = uri.host


fn set_connection_close(inout self):
self.headers[HeaderKey.CONNECTION] = "close"

Expand Down
2 changes: 1 addition & 1 deletion lightbug_http/http/response.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ struct HTTPResponse(Formattable, Stringable):
var status_text: String

try:
var properties = headers.parse_raw(reader)
var properties = headers.parse_raw(reader)
protocol, status_code, status_text = properties[0], properties[1], properties[2]
cookies.from_headers(properties[3])
reader.skip_newlines()
Expand Down
6 changes: 3 additions & 3 deletions lightbug_http/server.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ struct Server:
concurrency = DefaultConcurrency
return concurrency

fn listen_and_serve[T: HTTPService](inout self, address: String, handler: T) raises -> None:
fn listen_and_serve[T: HTTPService](inout self, address: String, inout handler: T) raises -> None:
"""
Listen for incoming connections and serve HTTP requests.
Expand All @@ -132,7 +132,7 @@ struct Server:
_ = self.set_address(address)
self.serve(listener, handler)

fn serve[T: HTTPService](inout self, ln: NoTLSListener, handler: T) raises -> None:
fn serve[T: HTTPService](inout self, ln: NoTLSListener, inout handler: T) raises -> None:
"""
Serve HTTP requests.
Expand All @@ -149,7 +149,7 @@ struct Server:
var conn = self.ln.accept()
self.serve_connection(conn, handler)

fn serve_connection[T: HTTPService](inout self, conn: SysConnection, handler: T) raises -> None:
fn serve_connection[T: HTTPService](inout self, conn: SysConnection, inout handler: T) raises -> None:
"""
Serve a single connection.
Expand Down
22 changes: 17 additions & 5 deletions lightbug_http/service.mojo
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ from lightbug_http.header import HeaderKey


trait HTTPService:
fn func(self, req: HTTPRequest) raises -> HTTPResponse:
fn func(inout self, req: HTTPRequest) raises -> HTTPResponse:
...


@value
struct Printer(HTTPService):
fn func(self, req: HTTPRequest) raises -> HTTPResponse:
fn func(inout self, req: HTTPRequest) raises -> HTTPResponse:
var uri = req.uri
print("Request URI: ", to_string(uri.request_uri))

Expand All @@ -28,7 +28,7 @@ struct Printer(HTTPService):

@value
struct Welcome(HTTPService):
fn func(self, req: HTTPRequest) raises -> HTTPResponse:
fn func(inout self, req: HTTPRequest) raises -> HTTPResponse:
var uri = req.uri

if uri.path == "/":
Expand All @@ -48,7 +48,7 @@ struct Welcome(HTTPService):

@value
struct ExampleRouter(HTTPService):
fn func(self, req: HTTPRequest) raises -> HTTPResponse:
fn func(inout self, req: HTTPRequest) raises -> HTTPResponse:
var body = req.body_raw
var uri = req.uri

Expand All @@ -66,7 +66,7 @@ struct ExampleRouter(HTTPService):

@value
struct TechEmpowerRouter(HTTPService):
fn func(self, req: HTTPRequest) raises -> HTTPResponse:
fn func(inout self, req: HTTPRequest) raises -> HTTPResponse:
var uri = req.uri

if uri.path == "/plaintext":
Expand All @@ -75,3 +75,15 @@ struct TechEmpowerRouter(HTTPService):
return OK('{"message": "Hello, World!"}', "application/json")

return OK("Hello world!") # text/plain is the default


@value
struct Counter(HTTPService):
var counter: Int

fn __init__(inout self):
self.counter = 0

fn func(inout self, req: HTTPRequest) raises -> HTTPResponse:
self.counter += 1
return OK("I have been called: " + str(self.counter) + " times")
8 changes: 3 additions & 5 deletions lightbug_http/strings.mojo
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from utils import Span
from utils import Span, StringSlice
from lightbug_http.io.bytes import Bytes
from lightbug_http.io.bytes import Bytes, bytes, byte

Expand Down Expand Up @@ -107,12 +107,10 @@ fn to_string(b: Span[UInt8]) -> String:
Args:
b: The Span of bytes to convert to a String.
"""
var bytes = List[UInt8, True](b)
bytes.append(0)
return String(bytes^)
return String(StringSlice(unsafe_from_utf8=b))


fn to_string(owned bytes: List[UInt8, True]) -> String:
fn to_string(owned bytes: Bytes) -> String:
"""Creates a String from the provided List of bytes.
If you do not transfer ownership of the List, the List will be copied.
Expand Down
Loading

0 comments on commit a108f32

Please sign in to comment.