From 70495fb4647218344176b5fb0ff246fa964c8780 Mon Sep 17 00:00:00 2001 From: Val Date: Sat, 28 Dec 2024 18:05:37 +0100 Subject: [PATCH 01/19] add recipe readme and image --- recipes/lightbug_http/README.md | 348 ++++++++++++++++++++++++++++++ recipes/lightbug_http/image.jpeg | Bin 0 -> 75201 bytes recipes/lightbug_http/recipe.yaml | 46 ++++ 3 files changed, 394 insertions(+) create mode 100644 recipes/lightbug_http/README.md create mode 100644 recipes/lightbug_http/image.jpeg create mode 100644 recipes/lightbug_http/recipe.yaml diff --git a/recipes/lightbug_http/README.md b/recipes/lightbug_http/README.md new file mode 100644 index 00000000..a160483e --- /dev/null +++ b/recipes/lightbug_http/README.md @@ -0,0 +1,348 @@ + + + +
+
+ +

Lightbug

+ +

+ 🐝 A Mojo HTTP framework with wings 🔥 +
+ + ![Written in Mojo][language-shield] + [![MIT License][license-shield]][license-url] + ![Build status][build-shield] +
+ [![Join our Discord][discord-shield]][discord-url] + [![Contributors Welcome][contributors-shield]][contributors-url] + + +

+
+ +## Overview + +Lightbug is a simple and sweet HTTP framework for Mojo that builds on best practice from systems programming, such as the Golang [FastHTTP](https://github.com/valyala/fasthttp/) and Rust [may_minihttp](https://github.com/Xudong-Huang/may_minihttp/). + +This is not production ready yet. We're aiming to keep up with new developments in Mojo, but it might take some time to get to a point when this is safe to use in real-world applications. + +Lightbug currently has the following features: + - [x] Pure Mojo networking! No dependencies on Python by default + - [x] TCP-based server and client implementation + - [x] Assign your own custom handler to a route + - [x] Craft HTTP requests and responses with built-in primitives + - [x] Everything is fully typed, with no `def` functions used + + ### Check Out These Mojo Libraries: + +- Logging - [@toasty/stump](https://github.com/thatstoasty/stump) +- CLI and Terminal - [@toasty/prism](https://github.com/thatstoasty/prism), [@toasty/mog](https://github.com/thatstoasty/mog) +- Date/Time - [@mojoto/morrow](https://github.com/mojoto/morrow.mojo) and [@toasty/small-time](https://github.com/thatstoasty/small-time) + +

(back to top)

+ + +## Getting Started + +The only hard dependency for `lightbug_http` is Mojo. +Learn how to get up and running with Mojo on the [Modular website](https://www.modular.com/max/mojo). +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.6" + ``` + +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(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(inout self, req: HTTPRequest) raises -> HTTPResponse: + var uri = req.uri + print("Request URI: ", to_string(uri.request_uri)) + + var header = req.headers + print("Request protocol: ", req.protocol) + print("Request method: ", req.method) + print( + "Request Content-Type: ", to_string(header[HeaderKey.CONTENT_TYPE]) + ) + + var body = req.body_raw + print("Request Body: ", to_string(body)) + + return OK(body) + ``` + +6. Start a server listening on a port with your service like so. + + ```mojo + from lightbug_http import Welcome, Server + + fn main() raises: + var server = Server() + 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(inout self, req: HTTPRequest) raises -> HTTPResponse: + var body = req.body_raw + var uri = req.uri + + if uri.path == "/": + print("I'm on the index path!") + if uri.path == "/first": + print("I'm on /first!") + elif uri.path == "/second": + print("I'm on /second!") + elif uri.path == "/echo": + print(to_string(body)) + + return OK(body) +``` + +We plan to add more advanced routing functionality in a future library called `lightbug_api`, see [Roadmap](#roadmap) for more details. + + +

(back to top)

+ +### Serving static files + +The default welcome screen shows an example of how to serve files like images or HTML using Lightbug. Mojo has built-in `open`, `read` and `read_bytes` methods that you can use to read files and serve them on a route. Assuming you copy an html file and image from the Lightbug repo into a `static` directory at the root of your repo: + +```mojo +from lightbug_http import * + +@value +struct Welcome(HTTPService): + fn func(inout self, req: HTTPRequest) raises -> HTTPResponse: + var uri = req.uri + + if uri.path == "/": + var html: Bytes + with open("static/lightbug_welcome.html", "r") as f: + html = f.read_bytes() + return OK(html, "text/html; charset=utf-8") + + if uri.path == "/logo.png": + var image: Bytes + with open("static/logo.png", "r") as f: + image = f.read_bytes() + return OK(image, "image/png") + + return NotFound(uri.path) +``` + +### Using the client + +Create a file, e.g `client.mojo` with the following code. Run `magic run mojo client.mojo` to execute the request to a given URL. + +```mojo +from lightbug_http import * +from lightbug_http.client import Client + +fn test_request(inout client: Client) raises -> None: + var uri = URI.parse_raises("http://httpbin.org/status/404") + var headers = Header("Host", "httpbin.org") + + var request = HTTPRequest(uri, headers) + var response = client.do(request^) + + # print status code + print("Response:", response.status_code) + + # print parsed headers (only some are parsed for now) + print("Content-Type:", response.headers["Content-Type"]) + print("Content-Length", response.headers["Content-Length"]) + print("Server:", to_string(response.headers["Server"])) + + print( + "Is connection set to connection-close? ", response.connection_close() + ) + + # print body + print(to_string(response.body_raw)) + + +fn main() -> None: + try: + var client = Client() + test_request(client) + except e: + print(e) +``` + +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! + + +## Roadmap + +
+ Logo +
+ +We're working on support for the following (contributors welcome!): + +- [ ] [WebSocket Support](https://github.com/saviorand/lightbug_http/pull/57) + - [ ] [SSL/HTTPS support](https://github.com/saviorand/lightbug_http/issues/20) + - [ ] UDP support + - [ ] [Better error handling](https://github.com/saviorand/lightbug_http/issues/3), [improved form/multipart and JSON support](https://github.com/saviorand/lightbug_http/issues/4) + - [ ] [Multiple simultaneous connections](https://github.com/saviorand/lightbug_http/issues/5), [parallelization and performance optimizations](https://github.com/saviorand/lightbug_http/issues/6) + - [ ] [HTTP 2.0/3.0 support](https://github.com/saviorand/lightbug_http/issues/8) + - [ ] [ASGI spec conformance](https://github.com/saviorand/lightbug_http/issues/17) + +The plan is to get to a feature set similar to Python frameworks like [Starlette](https://github.com/encode/starlette), but with better performance. + +Our vision is to develop three libraries, with `lightbug_http` (this repo) as a starting point: + - `lightbug_http` - HTTP infrastructure and basic API development + - `lightbug_api` - (coming later in 2024!) Tools to make great APIs fast, with support for OpenAPI spec and domain driven design + - `lightbug_web` - (release date TBD) Full-stack web framework for Mojo, similar to NextJS or SvelteKit + +The idea is to get to a point where the entire codebase of a simple modern web application can be written in Mojo. + +We don't make any promises, though -- this is just a vision, and whether we get there or not depends on many factors, including the support of the community. + + +See the [open issues](https://github.com/saviorand/lightbug_http/issues) and submit your own to help drive the development of Lightbug. + +

(back to top)

+ + + + +## Contributing + +Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details on how to contribute. + +If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". +Don't forget to give the project a star! + +1. Fork the Project +2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) +3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) +4. Push to the Branch (`git push origin feature/AmazingFeature`) +5. Open a Pull Request + +

(back to top)

+ + + + +## License + +Distributed under the MIT License. See `LICENSE.txt` for more information. + +

(back to top)

+ + + + +## Contact + +[Valentin Erokhin](https://www.valentin.wiki/) + +Project Link: [https://github.com/saviorand/mojo-web](https://github.com/saviorand/mojo-web) + +

(back to top)

+ + + + +## Acknowledgments + +We were drawing a lot on the following projects: + +* [FastHTTP](https://github.com/valyala/fasthttp/) (Golang) +* [may_minihttp](https://github.com/Xudong-Huang/may_minihttp/) (Rust) +* [FireTCP](https://github.com/Jensen-holm/FireTCP) (One of the earliest Mojo TCP implementations!) + + +

(back to top)

+ +## Contributors +Want your name to show up here? See [CONTRIBUTING.md](./CONTRIBUTING.md)! + + + + + +Made with [contrib.rocks](https://contrib.rocks). + + + +[build-shield]: https://img.shields.io/github/actions/workflow/status/saviorand/lightbug_http/.github%2Fworkflows%2Fpackage.yml +[language-shield]: https://img.shields.io/badge/language-mojo-orange +[license-shield]: https://img.shields.io/github/license/saviorand/lightbug_http?logo=github +[license-url]: https://github.com/saviorand/lightbug_http/blob/main/LICENSE +[contributors-shield]: https://img.shields.io/badge/contributors-welcome!-blue +[contributors-url]: https://github.com/saviorand/lightbug_http#contributing +[discord-shield]: https://img.shields.io/discord/1192127090271719495?style=flat&logo=discord&logoColor=white +[discord-url]: https://discord.gg/VFWETkTgrr diff --git a/recipes/lightbug_http/image.jpeg b/recipes/lightbug_http/image.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..da447076d4f063b3dffd3dca43994917d0184573 GIT binary patch literal 75201 zcmcG#1yEaG_bwdVt(4-@0>z5ETk+yffYRaw4<4XE3luM2EELy-;t<@66?X!~A-G#< zFTa^@?%eNw^WFQt_x|6N$(bbQ?5s8WtiASH>v_)K`M` zC;aj_F1SrULyv#yavXQ~D%!!ja zSEY|O^T@o59e+Ve(^*sp=~&s&2EiytZIUT0l%lLaP%Xe}l1SYXKf)AjoqxlOf7RUo z={oGFo7q!0HTy;s5t#~0$}>r+{>(k2$-uk84IWTgfdi8(rN33r9*(MqlnuSLgbtxNYI=) z1ps8>?UYQg4&jyx8Ynib+4J~@iw+23`p+Gtcuvnt*QaTE_KnJ*_ZH{+%SW$pBER-< zc-bT%Qz~O@GlF7)#nYZ`)Rl@Moxji*ph1mnX`1*}o8PKhOnK_A_p$oTXv2A_iepb7 zCdHpYUo&8G8FHK#ybSLtYP1Fia`<((&sW6?rA}Wxs1QgYD!hKaOxk1Ao87dD_rvjT z?dmteGUFk!_Ej&>_FO>0`+TNKd4e2IjPe~WDOQf*U_=>(|s`Xh&fhb+El}c2{@;EZe{%KaVC)Xd$5(&>I3XPk+~9) z;S#Lw6`-0=AqeQvD1IAABJ1A~EjY1?5%%o)R~wuE;v4@z#7rFdI)Z~cYXua&l9~{i z0no+UP)EWOGZ^&3!^$Us5+K7)g3)SVOnnK5);ISyq=l(}Sw6v5NzDFmDQ^dS^=ocr zu!!~SZS!X%|H7Ar=$~=X(IP2@M^aDQMR5k0i5Y3Tet@+VDCqpts68p@7<8Lu$fUF_ z4l<)x-5~{krWU>ebqV3Uz7ocSVzHhq49xL#7;V@M^=vrFK?>zb8f;aP!iyr%MQC!- z48l|8mYn?Q_z!+osZoqz(SX=^=1@$sQR@uW$^Tg7z#zS;&1Cn%c=U=tz@ob>>VX*K zWs}zK#z2Vp1s2-cvt^#j7MI}b&5n`i2r);RgP?Q~k>iBxn8+??7k#1YQE!XY1JfJ& zw0D)bkF$gChKZ#^UQmTFg?srfv1-rk!_K0<1>-CpfIC0__}Cc+Gl-NBaM}0QTfXq^ zQi;B)yfkRc%p5eetMr|@#Dc$SozjfOGn*O+?fP;5=Eu4+c0-{!1Cr=aB6~l$q%?(M zI$fOxlGC1-JUP=l_$xJXAxRXl-4sA7Z!}yY@t+E?LLUcKCO9&J8XdtpG9@M4XGpGV z`fLrRAE3xr4Prf!g+)eO8NXtShAEi`*p}Jamf;c+-|Zkd=Jk4v#$vhK>PtHD%*jgr zQiHF{Ti=E2m`ln|;h64&MABJDBihRz35aRjT*=fJ$ap zB?O`qImL4gw!m6?l}o^$6SsHh6tFy7Y>>gPdVZ~aB*bHc^hgPG1OCjhFC^YEizx8! zU#0|xA63t0?GDrW@v8dKJsCXui1{TCxhw7Kxd1JVGqvVm&-s35d-n%U&>Y0vd3!VLQ)U8_TEFHgLFmp&@WSq(O_~DTGUjF7tcvX1hx5Bhk zmcvp>i{wsNfNY(`&oj{4u6<6P1ITCmqTVP6&K<4try(g2qN61L!GHQWa_izp6y@B0f zZ^L(=#o8`zGD)y2D|D3Ys=5ztt$*<{M!0wqM61D@9B4}tP>imW@RuU5zwfw6X5+@n9 zsZJSdddgsLmmsWSNynhi53VAy66G1KYHOpX=k!ah5?GUdyq~4x^Xn?Ff{0nytFS(h zH~3y9UT@6Nj~atQ*CpU{)c#RIfq8nwDcNt|ySN(@K%N*fDVxZAN3QaZa6dl;Yd>r9 zWd1#E%itYhSpXdL7p?6QjT&mfnoQQ65uOnx&?iFNzMe}%z;l+1sMnIr7$n@j#{*y% z4xYp8s}q??UHfriw=UDO>O_k6rR2KiWgBIo8x;)W$`A%I=DZ(9F#}frft>?aW^d~O(#AKJ3D!= z@VCUy1x6i92XJyzytHUj3HoL(DJapFT}Uw2xGmhNZqFL24Gu6R^yr_@k9xFA z(WL(jqyoVC)_q&vitJMn_mx?1D4=!HEuv3S&2ywz2s04?Cn*xkjN`1p6$;A+)p@;u z6gq-{Qd$TsQ=)bncrt>~fo+klms%Zu<7<+0-Send!{p)M9m`go-&i zJO&$LDSt5YA&HJ;B`Dg|^uYO`!ew~*O1let`S6;{TvDLfxObN=c5dD&mj`oVFp`Rf zT-)6PuKtr*_h36kNRTHM&spqujoliP}q#q?$R%?8B@03dcf}uux0s zY>be(e7??{GaJa%o_!)mqXZtH`~WQB)%m>7&dRTfv*5lxS9b{t#boFUVl6^Fy_9sA zEj|mB?9HTi!M`i2SBe$<1&Xw-k=kG>r0G)W9khPs_rqO{$2LMwz&%|^GxQ{}LA{On zL&21QA-1vUzIC(7TfP-gBV+q}Uj`;pTzYxGwUGMsr`Jo>?G2ULp7(!t>X)7^j#fCE z32}mrJ_x945(Fmt|NsK8wnn zA1p3rdsY1IFM#H)cK3=abAviYUk4fRZI`IbKH1^qVP||#hQU4i@daH zY+8?hCL&eXx_97OukaNgIo3bWGHqu;DNG-QUf`RmbjX)Yun46IjA+kx% zWk&25R17WfZ<{Ty6lV#b5!jC3DQ~ENHQc%sx0>$1tIV&AtCg4HA-0KSW(uRo-@E*; z?IK{1A6~nOz~cAT?{LP=g(+Ug#%Cf~{!G!xzci(qYNY~P3^HMiqHP+-R6T8kc~|gU z_%!eR1=NWu{5VQAcs>#l9vfGA_I@~u1nGded31U|u+X8Ll{^^ZVa6Z-Kx7q0wYNdm zyNq@V%YLGP9p>rHU<2mp4Z}%F6iX^BfVCLwo5AX8rVVF?=B?sBvL1VM5@4U5Jxj?k zOR#aS8x?6U@Su5`2DCdoD6#i~2u})Knfz|%vXwQd%E>Owj#q^5tJFv@pp1VmYgCSz zw0Mpo5R8uQqJ&dxNExBU>s7r|ONVPEQVKbJj2iTo_N^BgEaSl8$Y%BIEp>HWNdAnO zM->iUWwGPVXQI?NoYL^^VbyJuDgp5cKGB8Z(tGX5$d{)pb^$iT8i*4&QfcMr0>k?s zx4gYagaJw$t$B<+>AUY^q5G@Z92{V8c8=x70zd^Y1{mpcz1>AUP^f*p8raf z?lLJRLEnSA>_W7{e#vy<&$Q!qmXZ@yMXvg6=4X1^^-TIB;D{d%^^Zr{&VSP~{$Jxy z&n(-P7)Bc$FoYw@iVMrYz8R{Wd*$>!%$Lqsqd0l)7&B-kOXjB!r5w8`r1b-JAxx=I z1YXu{Hsa0HbR-1rD*QM&q#Kj2Kwd`WEd+Fg@*f36x0}Uq!6l&e?E`y~t!ENa7@b05 z$4vnc&I5<9Gp@}t%9zsuEykdzIAvJ7w!v!4n>kmxF)x?Sy^+nzCOutXCTdPgd-*=+ zwT=n_!3zhFHb2rviT#3~ORsNV0E)n3cQv zdvL0nxY|*Wl6tm7mjVGE(W+;tyKAbOy8$6r2w4i2yX13)Y)K}rkWXsm7>q6nwnCCR zQ|+&xe_7c@3qpL0wSl@f{lN)V!D$b2Bk;h|e9_1pydi$faN^CwQj_wngUrh=Wp3PP zkqhNwtgglZa%AxmCRJaSVf)xu$eYK_6ianEuv$m4L z@#=f+qb~H`ewj0}HU}0x*y)3< zbQuyZrlqwn0pA?YAP=8NhqsD7p;62_c9gos3fEiMVPBYRG0_lz7)8yyB|zq;^IRr( zG+~vaJ#P+EL=XUQ+)mI&XSS+nyv(>s&7)fi2*%#g#?;0k4#u34qPf|ESDHQt?y+;e z^AfBs$VC@@8ykBh-f8MF*D`g&@am%fm~H2qFD?51?`o~#5{f)`I2!T9xyKzqiH-&9(Mo-IzCJLZ?% zv-vv)>#9kC|p48 z%{MtbfK9m#S!_rEn0}`uF}rl$m`Pz4_^|bpDuZ-w$8&{aCRY5qF=4 zu@ybFq5wh~>pP*Q*Q!2h$ zo`oI;42S+&f<^2bI#;@S2n!C=4}ATlB2&Ix+7HPvII^XPY|ijvi>5a$DPQ|Dr>AB% z52YK9>mZ$e4bD02|Mf)HrvClUIa$11^>f~;<<$gY1{qd970rP5;Y#OtkHAS8z5Ak< zz~ReytRAsne|Cs>zi;6wmgV9n<|=yofE#jhT(q(!)!(iMSsq=a-6hNzs;u%SAEF_P zMlW?DR;$%Nd9dY-mcWZtBOwT8T5%_1sm~j}^7gY0*7(^K4&sicj&@Tv^lE z*s+AWLb{}s&&`e(JWh8g=8lCCqdq1h+-y zo=x=7<3dhcrZ@i;JcVfB9bIm=fxGo(zoTUzTAA5aaX&fkaYTXRuJY*QU6YSo%sqOZ zzKN#Fprsn`uZ}~5Icz@S(B{hZed_1loKg)ziY_4apP5C<9IU$<7&Z0DOkW(HKh0!c z=r8$Xz?t+2sD1LJsQhj~Sn(~(u(BpDweuju7Edz2+?KEUBcaRwJKg;sZ!3gglGJ5P zvHsQWAXol6Ymb31Ur1^;#;CaRPCig-Yi8CX(yZ?9=sxr^lI-mp?z`@0PDYrAK z=8VN$&OI}+eCS>Ol){6q#>tU@ZfZ5GQNkl5ksxZozB#kOcRl#tsw^zmtV>tWarljK z0Kinp#(oi^A~?M0h-sZDWkh$_MpQ&wH#z)gCoc=_bjgVORMc>pvEsn+Ca?xq; z^y*`6g6P*@XLOQKjVC(pes<1wCF95-TDTCrqoyZ#(l;~U&eUv9a@ddeQ)12_?P&ak zlz|%k8Q}l4D>Te=U{`krKdL$2NCY%*JoS*Rpuyl+GVLB+T{b`w6M3U&1%cF#4_3sERV06jV)YB;uJapkQDaE zHx?UxIaaVzBM8?Lc)140CvbY4igR&B~68qckzyB z+E|f;PKk@Ae&P2-gKl05G&7E*49+7mJ^XJ>_?X>aapgGQp*wx6Fy#A8_Z5GfIn~lR zLN!DgckRbr`8nA_r6B&{#yh_K{=xFPR3RNF)vPuaE7zLMahVo`iB zdrwjN?{a2)o=kn+zDSJx*pXSQmtqr}>FY|wHonjn?$=c@b&XQbtSFmYjGz;dXpj{0 zHeEybhB{y})|@7ouYpQ5NUYtI*EDURY0et`%DJ{%4vA=SyobKjB#cWEr=c6D+k!i;<1&9$DEvRkI&lokE{&iQ zKBkw%Wi9fqjr!IjP(61%F#uZ9x;7IUD_UOg%T~{yC{7fRb#o6UNElvO7Izen_{FDu zjPYg8<9QtQfZzz8GS6!^UBxQ>(VRk;7QOvz4VQZA%2~E2m@Z<_qPCT_EBW8d(@%_S zCB&3#);B)9UHAZIP|?JcUGAr;VWwb+Kp5J}MrDV*(`EtwFnmEF_gIl(DW87^JW(6K zGF2CPH_A5NrXE|NrXMzJH*_FsMO<{(P&-#=HKJ&jUq7-O;q6>YTvBS^1T``L7%Mh6 zqBS}AV0xh)X-QLBijs)+J?LnzCP-BG%+5+i=DyY|!q?FW>z03EjxTOyb8UcT%oJuD z0uRx8iiah@@?DbYr?NCDg#u<6y3-F11UUfiUVil=O`TK{%lwXFpe({??|XSr5N2k9 zM)wbIRw?VC60~$|A@?{W=EtLsDmM2nhQEMtvGxG-nfl&ZoYnD7OQxG34PfCB%sM*e zQuvvllY^6OA*?tJnve6fDB9>*yJ$S|W5*j;YR)Iml2XXpD0#?!sBxB}(g`7)#?nHD z!+9GX@(F+QT225`hOUaOEkyO4MOU4l#l?NA*H&3qyPTDJh>?KT0;7T6xYnOqYmm{W zI#zgE_{o!9Y|l45_icU*birKDV3@O-IrUSZpxoU6;WhmM0(J)ucH8E$ph=;_Gn>L! z4zPxF4*(9SDh{*9fG;&tO6-?9v9xCP%aZX6T2<7cEWGaFWyusJ)js!S?27f$Xh{W~ zczx#y(<#>LjOBm7xl$ui&_GXKqhUOEsrduHtN^mR`vPlfM!by%XMvi@O_GRyoWaVk z=Cs;xR^fb0LHnjwEXnutTC3m2L5KicAuC<4Pkl&|S*iYIVaGnG ztlg<1w7hhk`7@y+K-Nd(f)0nl9&XhCG{tB;zrH7HD`EMDPyC%lSM2%tb7!W<5cIec zYRlnC1E|@SB}%8WIR>C^xNYj$sIh+{q4an9@BxQtA`jTE*?!E)9X>TMQGF#z%2R-B zu;d_?y!V#}ly8G$Cb_OY9f7C|Vp}VTTmG=?N(fO-5?q!Xb256>Z0w<_p17#&`}D8_YcK zu58_m+FG-vQ8Vg+O9h;916*6_+Ve;ff_DBR9~ZgB7~40`V$P|X4MAmJ5b^6jsgdiV zO{W-=hr6nnFc)<#5^Jzlt6j|kD*k`Rm9hD^xiV;RSJjPiST)h=<-oRW%GGe%oq`7b z1ju$UNgACQZd_4M*5|csTeMBes00c`*hBfz(+g3fWp5>oMyGvT;;_YuA@-$V?yZ)= zHc!tQ_Q@T8e$cFrg{Be29$Fr7hiZ00p)74O^X(~RR2XcMdsB1qop2{-t2nDRjlIoZ zZRH_!pwzFqKmCI>JQmb!Nd@yw1?iuh-$ruBL|yo&p&_2E1coj;*d zHs%Db+yC33jQ3qV$AKiS_tCm6 znkug_(JWq2B*_B|hp`m3t#VZDaf0&ef_ltQ65P@XQ*DM!&Iycb1J}7H)H1(`D@2AS zi?$rP+0U2NV0x3U?2WVoo!vqweK>^jCVqai-|Z+5(iq%jMk4=M80#8&{Z3$RAW5fq z=yj8>)>!@N=}<^P9ysWDg$SF*bGkgsZanB`} zk_n#?Eu9}&vCtFtE+k89BK12FUqkwQ5Y}FaRNUsQ+tG3*H*tWZ49gpk%RLRnA|-_j z7_*5hd}0l)kxmv+QIc{cjyDpEqNmlL>F z%9eZcV^tfG3T`VLnKIFR&;FQg<6+Won*DE;^8X)@BI>_`6mI_yNI@K#Aj1@>_O_nl zXv1+zbPxqk)oVglh2M{nND;j7UEgb!`3Z34@8OASNjvDO0uPVRWlGrTeXq0q=}SpI zlA4a|=_PE)sa`QGKC^5yfQwEs{8;OJ>pTdF-;=}bMi0l&P_$Zl=`(&D-(tCE@sL~J z>VMxEs?W=hD~)KOi@-5lyXpB{P^9BkCL5Gv-4jog*5L=|8aOGq%Zj7(=QlmcV2Kv*a#TiVXZJ>JQRhl9^%0{{$jJsKv#?0J#g?y&pl4HY0y?szg6WR zTAIVsyCJ*a>|l=iBZw)geUw$P{p65#OiO-Q=ER~Uv?mwdbSOmOeD+&KxT?IsQqnt-ZQa0;`Mn5TKL#jd|N~cOkCl>$hy>MIx6zZ!yz)A5h6FLb6 z*J*o&ORTSwo$4iMkorI}q3PbOlo8ntjlCP-Xs^$poZrGabFbmK*(#3@EfFdJ5#4NY zXLn|)vE~13cf)s6^%o9WNX_oV@GxJ+oVs|B?1vjZO{rHUu}Y*p3S-fmCd&Ex-n7n} zhN`t`Pos&=4a{)nZ0js8=@--btQriKT@5F3%k~@#c$177LP$oS80FpV(=Hc_Jh0v* zjp^a~nhJjzb&Z$$OEO(c-$&Vs*Jjeey0Ci8q?uv2Vs~$DL>PZ_q$G8c@sD4k5j2>t zollzY6I%xQ|lc@#TAQT{Mb_y11+s8 z^~__G(gS5YELEApF!kaRU(QMUr9k0tx@-5iKt13FxY-Omk*t6@v&{6^A*g$utdoa+ zv|wnH#GaS*94{3BEfQt2iHmd)-G{x?#5|E&+V#*kI&k+Psby^I^*q2w?i%r&)okw~ zvX2vlSJxU`QyANLB96KUo?FF>ga9zY_W0BZaBsUk!O6~>dOx0XVJso!m`-#(moW2} zEp>}zn6BKBSQV=luAgI+7JfnriOJNiH!c9D43YGdVKWE6qq7H{#|i?a9!Uey3lM4h zn=cPRghqAtD)=Eb->9X>$#t2G{g^nC%0A(7$>pKp7YB+~2-oT^?7uRO_K>Fc;t8`O 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ⓈfRqwfYt6KJJ!L_yqs$Cn%VhAQl#qHm7i3VW~Mdi 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 Date: Wed, 1 Jan 2025 17:36:51 +0100 Subject: [PATCH 02/19] update to hotfix version and fix recipe --- recipes/lightbug_http/README.md | 2 +- recipes/lightbug_http/recipe.yaml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/recipes/lightbug_http/README.md b/recipes/lightbug_http/README.md index a160483e..d30cfefa 100644 --- a/recipes/lightbug_http/README.md +++ b/recipes/lightbug_http/README.md @@ -60,7 +60,7 @@ Once you have a Mojo project set up locally, ```toml [dependencies] - lightbug_http = ">=0.1.6" + lightbug_http = ">=0.1.7" ``` 3. Run `magic install` at the root of your project, where `mojoproject.toml` is located diff --git a/recipes/lightbug_http/recipe.yaml b/recipes/lightbug_http/recipe.yaml index dd59de9f..97722dce 100644 --- a/recipes/lightbug_http/recipe.yaml +++ b/recipes/lightbug_http/recipe.yaml @@ -1,7 +1,7 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/prefix-dev/recipe-format/main/schema.json context: - version: "0.1.6" + version: "0.1.7" package: name: "lightbug_http" @@ -22,7 +22,7 @@ requirements: - max run: - ${{ pin_compatible('max') }} - - small_time == 0.1.3 + - small_time == 0.1.6 tests: - script: From ffad8b877e20abc97dc45690cac17a414edd88ab Mon Sep 17 00:00:00 2001 From: Val Date: Wed, 8 Jan 2025 19:10:25 +0100 Subject: [PATCH 03/19] add test files and bump version --- recipes/lightbug_http/README.md | 2 +- recipes/lightbug_http/recipe.yaml | 12 ++- .../tests/lightbug_http/io/test_bytes.mojo | 37 +++++++ .../tests/lightbug_http/test_client.mojo | 93 ++++++++++++++++++ .../tests/lightbug_http/test_cookie.mojo | 43 ++++++++ .../tests/lightbug_http/test_header.mojo | 56 +++++++++++ .../tests/lightbug_http/test_http.mojo | 98 +++++++++++++++++++ .../tests/lightbug_http/test_net.mojo | 7 ++ .../tests/lightbug_http/test_uri.mojo | 90 +++++++++++++++++ 9 files changed, 434 insertions(+), 4 deletions(-) create mode 100644 recipes/lightbug_http/tests/lightbug_http/io/test_bytes.mojo create mode 100644 recipes/lightbug_http/tests/lightbug_http/test_client.mojo create mode 100644 recipes/lightbug_http/tests/lightbug_http/test_cookie.mojo create mode 100644 recipes/lightbug_http/tests/lightbug_http/test_header.mojo create mode 100644 recipes/lightbug_http/tests/lightbug_http/test_http.mojo create mode 100644 recipes/lightbug_http/tests/lightbug_http/test_net.mojo create mode 100644 recipes/lightbug_http/tests/lightbug_http/test_uri.mojo diff --git a/recipes/lightbug_http/README.md b/recipes/lightbug_http/README.md index d30cfefa..29e8b715 100644 --- a/recipes/lightbug_http/README.md +++ b/recipes/lightbug_http/README.md @@ -60,7 +60,7 @@ Once you have a Mojo project set up locally, ```toml [dependencies] - lightbug_http = ">=0.1.7" + lightbug_http = ">=0.1.8" ``` 3. Run `magic install` at the root of your project, where `mojoproject.toml` is located diff --git a/recipes/lightbug_http/recipe.yaml b/recipes/lightbug_http/recipe.yaml index 97722dce..6d6c0690 100644 --- a/recipes/lightbug_http/recipe.yaml +++ b/recipes/lightbug_http/recipe.yaml @@ -1,7 +1,7 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/prefix-dev/recipe-format/main/schema.json context: - version: "0.1.7" + version: "0.1.8" package: name: "lightbug_http" @@ -30,8 +30,14 @@ tests: then: - magic run test files: - source: - - tests/lightbug_http/ + recipe: + - tests/lightbug_http/io/test_bytes.mojo + - tests/lightbug_http/test_client.mojo + - tests/lightbug_http/test_cookie.mojo + - tests/lightbug_http/test_header.mojo + - tests/lightbug_http/test_http.mojo + - tests/lightbug_http/test_net.mojo + - tests/lightbug_http/test_uri.mojo about: homepage: https://lightbug.site diff --git a/recipes/lightbug_http/tests/lightbug_http/io/test_bytes.mojo b/recipes/lightbug_http/tests/lightbug_http/io/test_bytes.mojo new file mode 100644 index 00000000..7e847d7b --- /dev/null +++ b/recipes/lightbug_http/tests/lightbug_http/io/test_bytes.mojo @@ -0,0 +1,37 @@ +import testing +from collections import Dict, List +from lightbug_http.io.bytes import Bytes, bytes + + +fn test_string_literal_to_bytes() raises: + var cases = Dict[StringLiteral, Bytes]() + cases[""] = Bytes() + cases["Hello world!"] = Bytes( + 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33 + ) + cases["\0"] = Bytes(0) + cases["\0\0\0\0"] = Bytes(0, 0, 0, 0) + cases["OK"] = Bytes(79, 75) + cases["HTTP/1.1 200 OK"] = Bytes( + 72, 84, 84, 80, 47, 49, 46, 49, 32, 50, 48, 48, 32, 79, 75 + ) + + for c in cases.items(): + testing.assert_equal(Bytes(c[].key.as_bytes()), c[].value) + + +fn test_string_to_bytes() raises: + var cases = Dict[String, Bytes]() + cases[String("")] = Bytes() + cases[String("Hello world!")] = Bytes( + 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33 + ) + cases[String("\0")] = Bytes(0) + cases[String("\0\0\0\0")] = Bytes(0, 0, 0, 0) + cases[String("OK")] = Bytes(79, 75) + cases[String("HTTP/1.1 200 OK")] = Bytes( + 72, 84, 84, 80, 47, 49, 46, 49, 32, 50, 48, 48, 32, 79, 75 + ) + + for c in cases.items(): + testing.assert_equal(Bytes(c[].key.as_bytes()), c[].value) diff --git a/recipes/lightbug_http/tests/lightbug_http/test_client.mojo b/recipes/lightbug_http/tests/lightbug_http/test_client.mojo new file mode 100644 index 00000000..c90384e4 --- /dev/null +++ b/recipes/lightbug_http/tests/lightbug_http/test_client.mojo @@ -0,0 +1,93 @@ +import testing +from lightbug_http.client import Client +from lightbug_http.http import HTTPRequest, encode +from lightbug_http.uri import URI +from lightbug_http.header import Header, Headers +from lightbug_http.io.bytes import bytes + + +fn test_mojo_client_redirect_external_req_google() raises: + var client = Client() + var req = HTTPRequest( + uri=URI.parse_raises("http://google.com"), + headers=Headers( + Header("Connection", "close")), + method="GET", + ) + try: + var res = client.do(req) + testing.assert_equal(res.status_code, 200) + except e: + print(e) + +fn test_mojo_client_redirect_external_req_302() raises: + var client = Client() + var req = HTTPRequest( + uri=URI.parse_raises("http://httpbin.org/status/302"), + headers=Headers( + Header("Connection", "close")), + method="GET", + ) + try: + var res = client.do(req) + testing.assert_equal(res.status_code, 200) + except e: + print(e) + +fn test_mojo_client_redirect_external_req_308() raises: + var client = Client() + var req = HTTPRequest( + uri=URI.parse_raises("http://httpbin.org/status/308"), + headers=Headers( + Header("Connection", "close")), + method="GET", + ) + try: + var res = client.do(req) + testing.assert_equal(res.status_code, 200) + except e: + print(e) + +fn test_mojo_client_redirect_external_req_307() raises: + var client = Client() + var req = HTTPRequest( + uri=URI.parse_raises("http://httpbin.org/status/307"), + headers=Headers( + Header("Connection", "close")), + method="GET", + ) + try: + var res = client.do(req) + testing.assert_equal(res.status_code, 200) + except e: + print(e) + +fn test_mojo_client_redirect_external_req_301() raises: + var client = Client() + var req = HTTPRequest( + uri=URI.parse_raises("http://httpbin.org/status/301"), + headers=Headers( + Header("Connection", "close")), + method="GET", + ) + try: + var res = client.do(req) + testing.assert_equal(res.status_code, 200) + testing.assert_equal(res.headers.content_length(), 228) + except e: + print(e) + +fn test_mojo_client_lightbug_external_req_200() raises: + try: + var client = Client() + var req = HTTPRequest( + uri=URI.parse_raises("http://httpbin.org/status/200"), + headers=Headers( + Header("Connection", "close")), + method="GET", + ) + var res = client.do(req) + testing.assert_equal(res.status_code, 200) + except e: + print(e) + raise diff --git a/recipes/lightbug_http/tests/lightbug_http/test_cookie.mojo b/recipes/lightbug_http/tests/lightbug_http/test_cookie.mojo new file mode 100644 index 00000000..6d3a21aa --- /dev/null +++ b/recipes/lightbug_http/tests/lightbug_http/test_cookie.mojo @@ -0,0 +1,43 @@ +from lightbug_http.cookie import SameSite, Cookie, Duration, Expiration +from small_time.small_time import SmallTime, now +from testing import assert_true, assert_equal +from collections import Optional + +fn test_set_cookie() raises: + cookie = Cookie( + name="mycookie", + value="myvalue", + max_age=Duration(minutes=20), + expires=Expiration.from_datetime(SmallTime(2037, 1, 22, 12, 0, 10, 0)), + path=str("/"), + domain=str("localhost"), + secure=True, + http_only=True, + same_site=SameSite.none, + partitioned=False + ) + var header = cookie.to_header() + var header_value = header.value + var expected = "mycookie=myvalue; Expires=Thu, 22 Jan 2037 12:00:10 GMT; Max-Age=1200; Domain=localhost; Path=/; Secure; HttpOnly; SameSite=none" + assert_equal("set-cookie", header.key) + assert_equal(header_value, expected) + + +fn test_set_cookie_partial_arguments() raises: + cookie = Cookie( + name="mycookie", + value="myvalue", + same_site=SameSite.lax + ) + var header = cookie.to_header() + var header_value = header.value + var expected = "mycookie=myvalue; SameSite=lax" + assert_equal("set-cookie", header.key) + assert_equal( header_value, expected) + + +fn test_expires_http_timestamp_format() raises: + var expected = "Thu, 22 Jan 2037 12:00:10 GMT" + var http_date = Expiration.from_datetime(SmallTime(2037, 1, 22, 12, 0, 10, 0)).http_date_timestamp() + assert_true(http_date is not None, msg="Http date is None") + assert_equal(expected , http_date.value()) diff --git a/recipes/lightbug_http/tests/lightbug_http/test_header.mojo b/recipes/lightbug_http/tests/lightbug_http/test_header.mojo new file mode 100644 index 00000000..5462aa32 --- /dev/null +++ b/recipes/lightbug_http/tests/lightbug_http/test_header.mojo @@ -0,0 +1,56 @@ +from testing import assert_equal, assert_true +from memory import Span +from lightbug_http.utils import ByteReader +from lightbug_http.header import Headers, Header +from lightbug_http.io.bytes import Bytes, bytes + + +def test_header_case_insensitive(): + var headers = Headers(Header("Host", "SomeHost")) + assert_true("host" in headers) + assert_true("HOST" in headers) + assert_true("hOST" in headers) + assert_equal(headers["Host"], "SomeHost") + assert_equal(headers["host"], "SomeHost") + + +def test_parse_request_header(): + var headers_str = bytes( + """GET /index.html HTTP/1.1\r\nHost:example.com\r\nUser-Agent: Mozilla/5.0\r\nContent-Type: text/html\r\nContent-Length: 1234\r\nConnection: close\r\nTrailer: end-of-message\r\n\r\n""" + ) + var header = Headers() + var b = Bytes(headers_str) + var reader = ByteReader(Span(b)) + var method: String + var protocol: String + var uri: String + var properties = header.parse_raw(reader) + method, uri, protocol = properties[0], properties[1], properties[2] + assert_equal(uri, "/index.html") + assert_equal(protocol, "HTTP/1.1") + assert_equal(method, "GET") + assert_equal(header["Host"], "example.com") + assert_equal(header["User-Agent"], "Mozilla/5.0") + assert_equal(header["Content-Type"], "text/html") + assert_equal(header["Content-Length"], "1234") + assert_equal(header["Connection"], "close") + + +def test_parse_response_header(): + var headers_str = "HTTP/1.1 200 OK\r\nServer: example.com\r\nUser-Agent: Mozilla/5.0\r\nContent-Type: text/html\r\nContent-Encoding: gzip\r\nContent-Length: 1234\r\nConnection: close\r\nTrailer: end-of-message\r\n\r\n" + var header = Headers() + var protocol: String + var status_code: String + var status_text: String + var reader = ByteReader(headers_str.as_bytes()) + var properties = header.parse_raw(reader) + protocol, status_code, status_text = properties[0], properties[1], properties[2] + assert_equal(protocol, "HTTP/1.1") + assert_equal(status_code, "200") + assert_equal(status_text, "OK") + assert_equal(header["Server"], "example.com") + assert_equal(header["Content-Type"], "text/html") + assert_equal(header["Content-Encoding"], "gzip") + assert_equal(header["Content-Length"], "1234") + assert_equal(header["Connection"], "close") + assert_equal(header["Trailer"], "end-of-message") diff --git a/recipes/lightbug_http/tests/lightbug_http/test_http.mojo b/recipes/lightbug_http/tests/lightbug_http/test_http.mojo new file mode 100644 index 00000000..55289594 --- /dev/null +++ b/recipes/lightbug_http/tests/lightbug_http/test_http.mojo @@ -0,0 +1,98 @@ +import testing +from testing import assert_true, assert_equal +from collections import Dict, List +from lightbug_http.io.bytes import Bytes, bytes +from lightbug_http.http import HTTPRequest, HTTPResponse, encode, HttpVersion +from lightbug_http.header import Header, Headers, HeaderKey +from lightbug_http.cookie import Cookie, ResponseCookieJar, RequestCookieJar, Duration, ResponseCookieKey +from lightbug_http.uri import URI +from lightbug_http.strings import to_string + +alias default_server_conn_string = "http://localhost:8080" + +def test_encode_http_request(): + var uri = URI.parse_raises(default_server_conn_string + "/foobar?baz") + var req = HTTPRequest( + uri, + body=String("Hello world!").as_bytes(), + cookies=RequestCookieJar( + Cookie(name="session_id", value="123", path=str("/"), secure=True, max_age=Duration(minutes=10)), + Cookie(name="token", value="abc", domain=str("localhost"), path=str("/api"), http_only=True) + ), + headers=Headers(Header("Connection", "keep-alive")), + ) + + var as_str = str(req) + var req_encoded = to_string(encode(req^)) + + + var expected = + "GET /foobar?baz HTTP/1.1\r\n" + "connection: keep-alive\r\n" + "content-length: 12\r\n" + "host: localhost:8080\r\n" + "cookie: session_id=123; token=abc\r\n" + "\r\n" + "Hello world!" + + testing.assert_equal( + req_encoded, + expected + ) + testing.assert_equal(req_encoded, as_str) + + +def test_encode_http_response(): + var res = HTTPResponse(bytes("Hello, World!")) + res.headers[HeaderKey.DATE] = "2024-06-02T13:41:50.766880+00:00" + + res.cookies = ResponseCookieJar( + Cookie(name="session_id", value="123", path=str("/api"), secure=True), + Cookie(name="session_id", value="abc", path=str("/"), secure=True, max_age=Duration(minutes=10)), + Cookie(name="token", value="123", domain=str("localhost"), path=str("/api"), http_only=True) + ) + var as_str = str(res) + var res_encoded = to_string(encode(res^)) + var expected_full = + "HTTP/1.1 200 OK\r\n" + "server: lightbug_http\r\n" + "content-type: application/octet-stream\r\n" + "connection: keep-alive\r\ncontent-length: 13\r\n" + "date: 2024-06-02T13:41:50.766880+00:00\r\n" + "set-cookie: session_id=123; Path=/api; Secure\r\n" + "set-cookie: session_id=abc; Max-Age=600; Path=/; Secure\r\n" + "set-cookie: token=123; Domain=localhost; Path=/api; HttpOnly\r\n" + "\r\n" + "Hello, World!" + + testing.assert_equal(res_encoded, expected_full) + testing.assert_equal(res_encoded, as_str) + +def test_decoding_http_response(): + var res = String( + "HTTP/1.1 200 OK\r\n" + "server: lightbug_http\r\n" + "content-type: application/octet-stream\r\n" + "connection: keep-alive\r\ncontent-length: 13\r\n" + "date: 2024-06-02T13:41:50.766880+00:00\r\n" + "set-cookie: session_id=123; Path=/; Secure\r\n" + "\r\n" + "Hello, World!" + ).as_bytes() + + var response = HTTPResponse.from_bytes(res) + var expected_cookie_key = ResponseCookieKey("session_id", "", "/") + + assert_equal(1, len(response.cookies)) + assert_true(expected_cookie_key in response.cookies, msg="request should contain a session_id header") + var session_id = response.cookies.get(expected_cookie_key) + assert_true(session_id is not None) + assert_equal(session_id.value().path.value(), "/") + assert_equal(200, response.status_code) + assert_equal("OK", response.status_text) + +def test_http_version_parse(): + var v1 = HttpVersion("HTTP/1.1") + testing.assert_equal(v1._v, 1) + var v2 = HttpVersion("HTTP/2") + testing.assert_equal(v2._v, 2) diff --git a/recipes/lightbug_http/tests/lightbug_http/test_net.mojo b/recipes/lightbug_http/tests/lightbug_http/test_net.mojo new file mode 100644 index 00000000..2a4d241b --- /dev/null +++ b/recipes/lightbug_http/tests/lightbug_http/test_net.mojo @@ -0,0 +1,7 @@ +def test_net(): + test_split_host_port() + + +def test_split_host_port(): + # TODO: Implement this test + ... diff --git a/recipes/lightbug_http/tests/lightbug_http/test_uri.mojo b/recipes/lightbug_http/tests/lightbug_http/test_uri.mojo new file mode 100644 index 00000000..885234b8 --- /dev/null +++ b/recipes/lightbug_http/tests/lightbug_http/test_uri.mojo @@ -0,0 +1,90 @@ +from utils import StringSlice +import testing +from lightbug_http.uri import URI +from lightbug_http.strings import empty_string, to_string +from lightbug_http.io.bytes import Bytes + + + +def test_uri_no_parse_defaults(): + var uri = URI.parse("http://example.com")[URI] + testing.assert_equal(uri.full_uri, "http://example.com") + + testing.assert_equal(uri.scheme, "http") + testing.assert_equal(uri.path, "/") + + +def test_uri_parse_http_with_port(): + var uri = URI.parse("http://example.com:8080/index.html")[URI] + testing.assert_equal(uri.scheme, "http") + testing.assert_equal(uri.host, "example.com:8080") + testing.assert_equal(uri.path, "/index.html") + testing.assert_equal(uri.__path_original, "/index.html") + testing.assert_equal(uri.request_uri, "/index.html") + testing.assert_equal(uri.is_https(), False) + testing.assert_equal(uri.is_http(), True) + testing.assert_equal(uri.query_string, empty_string) + + +def test_uri_parse_https_with_port(): + var uri = URI.parse("https://example.com:8080/index.html")[URI] + testing.assert_equal(uri.scheme, "https") + testing.assert_equal(uri.host, "example.com:8080") + testing.assert_equal(uri.path, "/index.html") + testing.assert_equal(uri.__path_original, "/index.html") + testing.assert_equal(uri.request_uri, "/index.html") + testing.assert_equal(uri.is_https(), True) + testing.assert_equal(uri.is_http(), False) + testing.assert_equal(uri.query_string, empty_string) + + +def test_uri_parse_http_with_path(): + var uri = URI.parse("http://example.com/index.html")[URI] + testing.assert_equal(uri.scheme, "http") + testing.assert_equal(uri.host, "example.com") + testing.assert_equal(uri.path, "/index.html") + testing.assert_equal(uri.__path_original, "/index.html") + testing.assert_equal(uri.request_uri, "/index.html") + testing.assert_equal(uri.is_https(), False) + testing.assert_equal(uri.is_http(), True) + testing.assert_equal(uri.query_string, empty_string) + + +def test_uri_parse_https_with_path(): + var uri = URI.parse("https://example.com/index.html")[URI] + testing.assert_equal(uri.scheme, "https") + testing.assert_equal(uri.host, "example.com") + testing.assert_equal(uri.path, "/index.html") + testing.assert_equal(uri.__path_original, "/index.html") + testing.assert_equal(uri.request_uri, "/index.html") + testing.assert_equal(uri.is_https(), True) + testing.assert_equal(uri.is_http(), False) + testing.assert_equal(uri.query_string, empty_string) + + +def test_uri_parse_http_basic(): + var uri = URI.parse("http://example.com")[URI] + testing.assert_equal(uri.scheme, "http") + testing.assert_equal(uri.host, "example.com") + testing.assert_equal(uri.path, "/") + testing.assert_equal(uri.__path_original, "/") + testing.assert_equal(uri.request_uri, "/") + testing.assert_equal(uri.query_string, empty_string) + + +def test_uri_parse_http_basic_www(): + var uri = URI.parse("http://www.example.com")[URI] + testing.assert_equal(uri.scheme, "http") + testing.assert_equal(uri.host, "www.example.com") + testing.assert_equal(uri.path, "/") + testing.assert_equal(uri.__path_original, "/") + testing.assert_equal(uri.request_uri, "/") + testing.assert_equal(uri.query_string, empty_string) + + +def test_uri_parse_http_with_query_string(): + ... + + +def test_uri_parse_http_with_hash(): + ... From b3290ee3ac1989b762d8573e0ae07da50e44b7a5 Mon Sep 17 00:00:00 2001 From: Val Date: Wed, 8 Jan 2025 19:11:24 +0100 Subject: [PATCH 04/19] update rev --- recipes/lightbug_http/recipe.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recipes/lightbug_http/recipe.yaml b/recipes/lightbug_http/recipe.yaml index 6d6c0690..e9941cc1 100644 --- a/recipes/lightbug_http/recipe.yaml +++ b/recipes/lightbug_http/recipe.yaml @@ -9,7 +9,7 @@ package: source: - git: https://github.com/saviorand/lightbug_http.git - rev: ea897f70483bf20ec3f4764308404e11d9271151 + rev: bcd2967c6177ac2c7800b0c4d51ddc0630326b28 build: number: 0 From 5d87180627473bfa700d0d346afc61c8df1b6e54 Mon Sep 17 00:00:00 2001 From: Val Date: Wed, 8 Jan 2025 19:12:49 +0100 Subject: [PATCH 05/19] add more maintainers --- recipes/lightbug_http/recipe.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/recipes/lightbug_http/recipe.yaml b/recipes/lightbug_http/recipe.yaml index e9941cc1..bed533c9 100644 --- a/recipes/lightbug_http/recipe.yaml +++ b/recipes/lightbug_http/recipe.yaml @@ -49,4 +49,6 @@ about: extra: maintainers: - saviorand + - bgreni + - thatstoasty project_name: lightbug_http From 98a3fa7d407f9b8cb7053bdb406e9b2a98d79239 Mon Sep 17 00:00:00 2001 From: Val Date: Sat, 11 Jan 2025 15:05:22 +0100 Subject: [PATCH 06/19] remove README --- recipes/lightbug_http/README.md | 348 -------------------------------- 1 file changed, 348 deletions(-) delete mode 100644 recipes/lightbug_http/README.md diff --git a/recipes/lightbug_http/README.md b/recipes/lightbug_http/README.md deleted file mode 100644 index 29e8b715..00000000 --- a/recipes/lightbug_http/README.md +++ /dev/null @@ -1,348 +0,0 @@ - - - -
-
- -

Lightbug

- -

- 🐝 A Mojo HTTP framework with wings 🔥 -
- - ![Written in Mojo][language-shield] - [![MIT License][license-shield]][license-url] - ![Build status][build-shield] -
- [![Join our Discord][discord-shield]][discord-url] - [![Contributors Welcome][contributors-shield]][contributors-url] - - -

-
- -## Overview - -Lightbug is a simple and sweet HTTP framework for Mojo that builds on best practice from systems programming, such as the Golang [FastHTTP](https://github.com/valyala/fasthttp/) and Rust [may_minihttp](https://github.com/Xudong-Huang/may_minihttp/). - -This is not production ready yet. We're aiming to keep up with new developments in Mojo, but it might take some time to get to a point when this is safe to use in real-world applications. - -Lightbug currently has the following features: - - [x] Pure Mojo networking! No dependencies on Python by default - - [x] TCP-based server and client implementation - - [x] Assign your own custom handler to a route - - [x] Craft HTTP requests and responses with built-in primitives - - [x] Everything is fully typed, with no `def` functions used - - ### Check Out These Mojo Libraries: - -- Logging - [@toasty/stump](https://github.com/thatstoasty/stump) -- CLI and Terminal - [@toasty/prism](https://github.com/thatstoasty/prism), [@toasty/mog](https://github.com/thatstoasty/mog) -- Date/Time - [@mojoto/morrow](https://github.com/mojoto/morrow.mojo) and [@toasty/small-time](https://github.com/thatstoasty/small-time) - -

(back to top)

- - -## Getting Started - -The only hard dependency for `lightbug_http` is Mojo. -Learn how to get up and running with Mojo on the [Modular website](https://www.modular.com/max/mojo). -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.8" - ``` - -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(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(inout self, req: HTTPRequest) raises -> HTTPResponse: - var uri = req.uri - print("Request URI: ", to_string(uri.request_uri)) - - var header = req.headers - print("Request protocol: ", req.protocol) - print("Request method: ", req.method) - print( - "Request Content-Type: ", to_string(header[HeaderKey.CONTENT_TYPE]) - ) - - var body = req.body_raw - print("Request Body: ", to_string(body)) - - return OK(body) - ``` - -6. Start a server listening on a port with your service like so. - - ```mojo - from lightbug_http import Welcome, Server - - fn main() raises: - var server = Server() - 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(inout self, req: HTTPRequest) raises -> HTTPResponse: - var body = req.body_raw - var uri = req.uri - - if uri.path == "/": - print("I'm on the index path!") - if uri.path == "/first": - print("I'm on /first!") - elif uri.path == "/second": - print("I'm on /second!") - elif uri.path == "/echo": - print(to_string(body)) - - return OK(body) -``` - -We plan to add more advanced routing functionality in a future library called `lightbug_api`, see [Roadmap](#roadmap) for more details. - - -

(back to top)

- -### Serving static files - -The default welcome screen shows an example of how to serve files like images or HTML using Lightbug. Mojo has built-in `open`, `read` and `read_bytes` methods that you can use to read files and serve them on a route. Assuming you copy an html file and image from the Lightbug repo into a `static` directory at the root of your repo: - -```mojo -from lightbug_http import * - -@value -struct Welcome(HTTPService): - fn func(inout self, req: HTTPRequest) raises -> HTTPResponse: - var uri = req.uri - - if uri.path == "/": - var html: Bytes - with open("static/lightbug_welcome.html", "r") as f: - html = f.read_bytes() - return OK(html, "text/html; charset=utf-8") - - if uri.path == "/logo.png": - var image: Bytes - with open("static/logo.png", "r") as f: - image = f.read_bytes() - return OK(image, "image/png") - - return NotFound(uri.path) -``` - -### Using the client - -Create a file, e.g `client.mojo` with the following code. Run `magic run mojo client.mojo` to execute the request to a given URL. - -```mojo -from lightbug_http import * -from lightbug_http.client import Client - -fn test_request(inout client: Client) raises -> None: - var uri = URI.parse_raises("http://httpbin.org/status/404") - var headers = Header("Host", "httpbin.org") - - var request = HTTPRequest(uri, headers) - var response = client.do(request^) - - # print status code - print("Response:", response.status_code) - - # print parsed headers (only some are parsed for now) - print("Content-Type:", response.headers["Content-Type"]) - print("Content-Length", response.headers["Content-Length"]) - print("Server:", to_string(response.headers["Server"])) - - print( - "Is connection set to connection-close? ", response.connection_close() - ) - - # print body - print(to_string(response.body_raw)) - - -fn main() -> None: - try: - var client = Client() - test_request(client) - except e: - print(e) -``` - -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! - - -## Roadmap - -
- Logo -
- -We're working on support for the following (contributors welcome!): - -- [ ] [WebSocket Support](https://github.com/saviorand/lightbug_http/pull/57) - - [ ] [SSL/HTTPS support](https://github.com/saviorand/lightbug_http/issues/20) - - [ ] UDP support - - [ ] [Better error handling](https://github.com/saviorand/lightbug_http/issues/3), [improved form/multipart and JSON support](https://github.com/saviorand/lightbug_http/issues/4) - - [ ] [Multiple simultaneous connections](https://github.com/saviorand/lightbug_http/issues/5), [parallelization and performance optimizations](https://github.com/saviorand/lightbug_http/issues/6) - - [ ] [HTTP 2.0/3.0 support](https://github.com/saviorand/lightbug_http/issues/8) - - [ ] [ASGI spec conformance](https://github.com/saviorand/lightbug_http/issues/17) - -The plan is to get to a feature set similar to Python frameworks like [Starlette](https://github.com/encode/starlette), but with better performance. - -Our vision is to develop three libraries, with `lightbug_http` (this repo) as a starting point: - - `lightbug_http` - HTTP infrastructure and basic API development - - `lightbug_api` - (coming later in 2024!) Tools to make great APIs fast, with support for OpenAPI spec and domain driven design - - `lightbug_web` - (release date TBD) Full-stack web framework for Mojo, similar to NextJS or SvelteKit - -The idea is to get to a point where the entire codebase of a simple modern web application can be written in Mojo. - -We don't make any promises, though -- this is just a vision, and whether we get there or not depends on many factors, including the support of the community. - - -See the [open issues](https://github.com/saviorand/lightbug_http/issues) and submit your own to help drive the development of Lightbug. - -

(back to top)

- - - - -## Contributing - -Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details on how to contribute. - -If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". -Don't forget to give the project a star! - -1. Fork the Project -2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) -3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) -4. Push to the Branch (`git push origin feature/AmazingFeature`) -5. Open a Pull Request - -

(back to top)

- - - - -## License - -Distributed under the MIT License. See `LICENSE.txt` for more information. - -

(back to top)

- - - - -## Contact - -[Valentin Erokhin](https://www.valentin.wiki/) - -Project Link: [https://github.com/saviorand/mojo-web](https://github.com/saviorand/mojo-web) - -

(back to top)

- - - - -## Acknowledgments - -We were drawing a lot on the following projects: - -* [FastHTTP](https://github.com/valyala/fasthttp/) (Golang) -* [may_minihttp](https://github.com/Xudong-Huang/may_minihttp/) (Rust) -* [FireTCP](https://github.com/Jensen-holm/FireTCP) (One of the earliest Mojo TCP implementations!) - - -

(back to top)

- -## Contributors -Want your name to show up here? See [CONTRIBUTING.md](./CONTRIBUTING.md)! - - - - - -Made with [contrib.rocks](https://contrib.rocks). - - - -[build-shield]: https://img.shields.io/github/actions/workflow/status/saviorand/lightbug_http/.github%2Fworkflows%2Fpackage.yml -[language-shield]: https://img.shields.io/badge/language-mojo-orange -[license-shield]: https://img.shields.io/github/license/saviorand/lightbug_http?logo=github -[license-url]: https://github.com/saviorand/lightbug_http/blob/main/LICENSE -[contributors-shield]: https://img.shields.io/badge/contributors-welcome!-blue -[contributors-url]: https://github.com/saviorand/lightbug_http#contributing -[discord-shield]: https://img.shields.io/discord/1192127090271719495?style=flat&logo=discord&logoColor=white -[discord-url]: https://discord.gg/VFWETkTgrr From c04fcf7f1ccba0b93ed0deac36f8e2d882233dcf Mon Sep 17 00:00:00 2001 From: Val Date: Tue, 14 Jan 2025 20:57:06 +0100 Subject: [PATCH 07/19] update build script --- recipes/lightbug_http/recipe.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recipes/lightbug_http/recipe.yaml b/recipes/lightbug_http/recipe.yaml index bed533c9..3d302cc5 100644 --- a/recipes/lightbug_http/recipe.yaml +++ b/recipes/lightbug_http/recipe.yaml @@ -15,7 +15,7 @@ build: number: 0 script: - mkdir -p ${PREFIX}/lib/mojo - - magic run mojo package . -o ${PREFIX}/lib/mojo/lightbug_http.mojopkg + - mojo package lightbug_http -o ${{ PREFIX }}/lib/mojo/lightbug_http.mojopkg requirements: host: From 9d1a8c4e89bd1e19846b5211b6315905a5ecd40a Mon Sep 17 00:00:00 2001 From: Val Date: Thu, 16 Jan 2025 18:18:40 +0100 Subject: [PATCH 08/19] remove tests --- .../tests/lightbug_http/io/test_bytes.mojo | 37 ------- .../tests/lightbug_http/test_client.mojo | 93 ------------------ .../tests/lightbug_http/test_cookie.mojo | 43 -------- .../tests/lightbug_http/test_header.mojo | 56 ----------- .../tests/lightbug_http/test_http.mojo | 98 ------------------- .../tests/lightbug_http/test_net.mojo | 7 -- .../tests/lightbug_http/test_uri.mojo | 90 ----------------- 7 files changed, 424 deletions(-) delete mode 100644 recipes/lightbug_http/tests/lightbug_http/io/test_bytes.mojo delete mode 100644 recipes/lightbug_http/tests/lightbug_http/test_client.mojo delete mode 100644 recipes/lightbug_http/tests/lightbug_http/test_cookie.mojo delete mode 100644 recipes/lightbug_http/tests/lightbug_http/test_header.mojo delete mode 100644 recipes/lightbug_http/tests/lightbug_http/test_http.mojo delete mode 100644 recipes/lightbug_http/tests/lightbug_http/test_net.mojo delete mode 100644 recipes/lightbug_http/tests/lightbug_http/test_uri.mojo diff --git a/recipes/lightbug_http/tests/lightbug_http/io/test_bytes.mojo b/recipes/lightbug_http/tests/lightbug_http/io/test_bytes.mojo deleted file mode 100644 index 7e847d7b..00000000 --- a/recipes/lightbug_http/tests/lightbug_http/io/test_bytes.mojo +++ /dev/null @@ -1,37 +0,0 @@ -import testing -from collections import Dict, List -from lightbug_http.io.bytes import Bytes, bytes - - -fn test_string_literal_to_bytes() raises: - var cases = Dict[StringLiteral, Bytes]() - cases[""] = Bytes() - cases["Hello world!"] = Bytes( - 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33 - ) - cases["\0"] = Bytes(0) - cases["\0\0\0\0"] = Bytes(0, 0, 0, 0) - cases["OK"] = Bytes(79, 75) - cases["HTTP/1.1 200 OK"] = Bytes( - 72, 84, 84, 80, 47, 49, 46, 49, 32, 50, 48, 48, 32, 79, 75 - ) - - for c in cases.items(): - testing.assert_equal(Bytes(c[].key.as_bytes()), c[].value) - - -fn test_string_to_bytes() raises: - var cases = Dict[String, Bytes]() - cases[String("")] = Bytes() - cases[String("Hello world!")] = Bytes( - 72, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33 - ) - cases[String("\0")] = Bytes(0) - cases[String("\0\0\0\0")] = Bytes(0, 0, 0, 0) - cases[String("OK")] = Bytes(79, 75) - cases[String("HTTP/1.1 200 OK")] = Bytes( - 72, 84, 84, 80, 47, 49, 46, 49, 32, 50, 48, 48, 32, 79, 75 - ) - - for c in cases.items(): - testing.assert_equal(Bytes(c[].key.as_bytes()), c[].value) diff --git a/recipes/lightbug_http/tests/lightbug_http/test_client.mojo b/recipes/lightbug_http/tests/lightbug_http/test_client.mojo deleted file mode 100644 index c90384e4..00000000 --- a/recipes/lightbug_http/tests/lightbug_http/test_client.mojo +++ /dev/null @@ -1,93 +0,0 @@ -import testing -from lightbug_http.client import Client -from lightbug_http.http import HTTPRequest, encode -from lightbug_http.uri import URI -from lightbug_http.header import Header, Headers -from lightbug_http.io.bytes import bytes - - -fn test_mojo_client_redirect_external_req_google() raises: - var client = Client() - var req = HTTPRequest( - uri=URI.parse_raises("http://google.com"), - headers=Headers( - Header("Connection", "close")), - method="GET", - ) - try: - var res = client.do(req) - testing.assert_equal(res.status_code, 200) - except e: - print(e) - -fn test_mojo_client_redirect_external_req_302() raises: - var client = Client() - var req = HTTPRequest( - uri=URI.parse_raises("http://httpbin.org/status/302"), - headers=Headers( - Header("Connection", "close")), - method="GET", - ) - try: - var res = client.do(req) - testing.assert_equal(res.status_code, 200) - except e: - print(e) - -fn test_mojo_client_redirect_external_req_308() raises: - var client = Client() - var req = HTTPRequest( - uri=URI.parse_raises("http://httpbin.org/status/308"), - headers=Headers( - Header("Connection", "close")), - method="GET", - ) - try: - var res = client.do(req) - testing.assert_equal(res.status_code, 200) - except e: - print(e) - -fn test_mojo_client_redirect_external_req_307() raises: - var client = Client() - var req = HTTPRequest( - uri=URI.parse_raises("http://httpbin.org/status/307"), - headers=Headers( - Header("Connection", "close")), - method="GET", - ) - try: - var res = client.do(req) - testing.assert_equal(res.status_code, 200) - except e: - print(e) - -fn test_mojo_client_redirect_external_req_301() raises: - var client = Client() - var req = HTTPRequest( - uri=URI.parse_raises("http://httpbin.org/status/301"), - headers=Headers( - Header("Connection", "close")), - method="GET", - ) - try: - var res = client.do(req) - testing.assert_equal(res.status_code, 200) - testing.assert_equal(res.headers.content_length(), 228) - except e: - print(e) - -fn test_mojo_client_lightbug_external_req_200() raises: - try: - var client = Client() - var req = HTTPRequest( - uri=URI.parse_raises("http://httpbin.org/status/200"), - headers=Headers( - Header("Connection", "close")), - method="GET", - ) - var res = client.do(req) - testing.assert_equal(res.status_code, 200) - except e: - print(e) - raise diff --git a/recipes/lightbug_http/tests/lightbug_http/test_cookie.mojo b/recipes/lightbug_http/tests/lightbug_http/test_cookie.mojo deleted file mode 100644 index 6d3a21aa..00000000 --- a/recipes/lightbug_http/tests/lightbug_http/test_cookie.mojo +++ /dev/null @@ -1,43 +0,0 @@ -from lightbug_http.cookie import SameSite, Cookie, Duration, Expiration -from small_time.small_time import SmallTime, now -from testing import assert_true, assert_equal -from collections import Optional - -fn test_set_cookie() raises: - cookie = Cookie( - name="mycookie", - value="myvalue", - max_age=Duration(minutes=20), - expires=Expiration.from_datetime(SmallTime(2037, 1, 22, 12, 0, 10, 0)), - path=str("/"), - domain=str("localhost"), - secure=True, - http_only=True, - same_site=SameSite.none, - partitioned=False - ) - var header = cookie.to_header() - var header_value = header.value - var expected = "mycookie=myvalue; Expires=Thu, 22 Jan 2037 12:00:10 GMT; Max-Age=1200; Domain=localhost; Path=/; Secure; HttpOnly; SameSite=none" - assert_equal("set-cookie", header.key) - assert_equal(header_value, expected) - - -fn test_set_cookie_partial_arguments() raises: - cookie = Cookie( - name="mycookie", - value="myvalue", - same_site=SameSite.lax - ) - var header = cookie.to_header() - var header_value = header.value - var expected = "mycookie=myvalue; SameSite=lax" - assert_equal("set-cookie", header.key) - assert_equal( header_value, expected) - - -fn test_expires_http_timestamp_format() raises: - var expected = "Thu, 22 Jan 2037 12:00:10 GMT" - var http_date = Expiration.from_datetime(SmallTime(2037, 1, 22, 12, 0, 10, 0)).http_date_timestamp() - assert_true(http_date is not None, msg="Http date is None") - assert_equal(expected , http_date.value()) diff --git a/recipes/lightbug_http/tests/lightbug_http/test_header.mojo b/recipes/lightbug_http/tests/lightbug_http/test_header.mojo deleted file mode 100644 index 5462aa32..00000000 --- a/recipes/lightbug_http/tests/lightbug_http/test_header.mojo +++ /dev/null @@ -1,56 +0,0 @@ -from testing import assert_equal, assert_true -from memory import Span -from lightbug_http.utils import ByteReader -from lightbug_http.header import Headers, Header -from lightbug_http.io.bytes import Bytes, bytes - - -def test_header_case_insensitive(): - var headers = Headers(Header("Host", "SomeHost")) - assert_true("host" in headers) - assert_true("HOST" in headers) - assert_true("hOST" in headers) - assert_equal(headers["Host"], "SomeHost") - assert_equal(headers["host"], "SomeHost") - - -def test_parse_request_header(): - var headers_str = bytes( - """GET /index.html HTTP/1.1\r\nHost:example.com\r\nUser-Agent: Mozilla/5.0\r\nContent-Type: text/html\r\nContent-Length: 1234\r\nConnection: close\r\nTrailer: end-of-message\r\n\r\n""" - ) - var header = Headers() - var b = Bytes(headers_str) - var reader = ByteReader(Span(b)) - var method: String - var protocol: String - var uri: String - var properties = header.parse_raw(reader) - method, uri, protocol = properties[0], properties[1], properties[2] - assert_equal(uri, "/index.html") - assert_equal(protocol, "HTTP/1.1") - assert_equal(method, "GET") - assert_equal(header["Host"], "example.com") - assert_equal(header["User-Agent"], "Mozilla/5.0") - assert_equal(header["Content-Type"], "text/html") - assert_equal(header["Content-Length"], "1234") - assert_equal(header["Connection"], "close") - - -def test_parse_response_header(): - var headers_str = "HTTP/1.1 200 OK\r\nServer: example.com\r\nUser-Agent: Mozilla/5.0\r\nContent-Type: text/html\r\nContent-Encoding: gzip\r\nContent-Length: 1234\r\nConnection: close\r\nTrailer: end-of-message\r\n\r\n" - var header = Headers() - var protocol: String - var status_code: String - var status_text: String - var reader = ByteReader(headers_str.as_bytes()) - var properties = header.parse_raw(reader) - protocol, status_code, status_text = properties[0], properties[1], properties[2] - assert_equal(protocol, "HTTP/1.1") - assert_equal(status_code, "200") - assert_equal(status_text, "OK") - assert_equal(header["Server"], "example.com") - assert_equal(header["Content-Type"], "text/html") - assert_equal(header["Content-Encoding"], "gzip") - assert_equal(header["Content-Length"], "1234") - assert_equal(header["Connection"], "close") - assert_equal(header["Trailer"], "end-of-message") diff --git a/recipes/lightbug_http/tests/lightbug_http/test_http.mojo b/recipes/lightbug_http/tests/lightbug_http/test_http.mojo deleted file mode 100644 index 55289594..00000000 --- a/recipes/lightbug_http/tests/lightbug_http/test_http.mojo +++ /dev/null @@ -1,98 +0,0 @@ -import testing -from testing import assert_true, assert_equal -from collections import Dict, List -from lightbug_http.io.bytes import Bytes, bytes -from lightbug_http.http import HTTPRequest, HTTPResponse, encode, HttpVersion -from lightbug_http.header import Header, Headers, HeaderKey -from lightbug_http.cookie import Cookie, ResponseCookieJar, RequestCookieJar, Duration, ResponseCookieKey -from lightbug_http.uri import URI -from lightbug_http.strings import to_string - -alias default_server_conn_string = "http://localhost:8080" - -def test_encode_http_request(): - var uri = URI.parse_raises(default_server_conn_string + "/foobar?baz") - var req = HTTPRequest( - uri, - body=String("Hello world!").as_bytes(), - cookies=RequestCookieJar( - Cookie(name="session_id", value="123", path=str("/"), secure=True, max_age=Duration(minutes=10)), - Cookie(name="token", value="abc", domain=str("localhost"), path=str("/api"), http_only=True) - ), - headers=Headers(Header("Connection", "keep-alive")), - ) - - var as_str = str(req) - var req_encoded = to_string(encode(req^)) - - - var expected = - "GET /foobar?baz HTTP/1.1\r\n" - "connection: keep-alive\r\n" - "content-length: 12\r\n" - "host: localhost:8080\r\n" - "cookie: session_id=123; token=abc\r\n" - "\r\n" - "Hello world!" - - testing.assert_equal( - req_encoded, - expected - ) - testing.assert_equal(req_encoded, as_str) - - -def test_encode_http_response(): - var res = HTTPResponse(bytes("Hello, World!")) - res.headers[HeaderKey.DATE] = "2024-06-02T13:41:50.766880+00:00" - - res.cookies = ResponseCookieJar( - Cookie(name="session_id", value="123", path=str("/api"), secure=True), - Cookie(name="session_id", value="abc", path=str("/"), secure=True, max_age=Duration(minutes=10)), - Cookie(name="token", value="123", domain=str("localhost"), path=str("/api"), http_only=True) - ) - var as_str = str(res) - var res_encoded = to_string(encode(res^)) - var expected_full = - "HTTP/1.1 200 OK\r\n" - "server: lightbug_http\r\n" - "content-type: application/octet-stream\r\n" - "connection: keep-alive\r\ncontent-length: 13\r\n" - "date: 2024-06-02T13:41:50.766880+00:00\r\n" - "set-cookie: session_id=123; Path=/api; Secure\r\n" - "set-cookie: session_id=abc; Max-Age=600; Path=/; Secure\r\n" - "set-cookie: token=123; Domain=localhost; Path=/api; HttpOnly\r\n" - "\r\n" - "Hello, World!" - - testing.assert_equal(res_encoded, expected_full) - testing.assert_equal(res_encoded, as_str) - -def test_decoding_http_response(): - var res = String( - "HTTP/1.1 200 OK\r\n" - "server: lightbug_http\r\n" - "content-type: application/octet-stream\r\n" - "connection: keep-alive\r\ncontent-length: 13\r\n" - "date: 2024-06-02T13:41:50.766880+00:00\r\n" - "set-cookie: session_id=123; Path=/; Secure\r\n" - "\r\n" - "Hello, World!" - ).as_bytes() - - var response = HTTPResponse.from_bytes(res) - var expected_cookie_key = ResponseCookieKey("session_id", "", "/") - - assert_equal(1, len(response.cookies)) - assert_true(expected_cookie_key in response.cookies, msg="request should contain a session_id header") - var session_id = response.cookies.get(expected_cookie_key) - assert_true(session_id is not None) - assert_equal(session_id.value().path.value(), "/") - assert_equal(200, response.status_code) - assert_equal("OK", response.status_text) - -def test_http_version_parse(): - var v1 = HttpVersion("HTTP/1.1") - testing.assert_equal(v1._v, 1) - var v2 = HttpVersion("HTTP/2") - testing.assert_equal(v2._v, 2) diff --git a/recipes/lightbug_http/tests/lightbug_http/test_net.mojo b/recipes/lightbug_http/tests/lightbug_http/test_net.mojo deleted file mode 100644 index 2a4d241b..00000000 --- a/recipes/lightbug_http/tests/lightbug_http/test_net.mojo +++ /dev/null @@ -1,7 +0,0 @@ -def test_net(): - test_split_host_port() - - -def test_split_host_port(): - # TODO: Implement this test - ... diff --git a/recipes/lightbug_http/tests/lightbug_http/test_uri.mojo b/recipes/lightbug_http/tests/lightbug_http/test_uri.mojo deleted file mode 100644 index 885234b8..00000000 --- a/recipes/lightbug_http/tests/lightbug_http/test_uri.mojo +++ /dev/null @@ -1,90 +0,0 @@ -from utils import StringSlice -import testing -from lightbug_http.uri import URI -from lightbug_http.strings import empty_string, to_string -from lightbug_http.io.bytes import Bytes - - - -def test_uri_no_parse_defaults(): - var uri = URI.parse("http://example.com")[URI] - testing.assert_equal(uri.full_uri, "http://example.com") - - testing.assert_equal(uri.scheme, "http") - testing.assert_equal(uri.path, "/") - - -def test_uri_parse_http_with_port(): - var uri = URI.parse("http://example.com:8080/index.html")[URI] - testing.assert_equal(uri.scheme, "http") - testing.assert_equal(uri.host, "example.com:8080") - testing.assert_equal(uri.path, "/index.html") - testing.assert_equal(uri.__path_original, "/index.html") - testing.assert_equal(uri.request_uri, "/index.html") - testing.assert_equal(uri.is_https(), False) - testing.assert_equal(uri.is_http(), True) - testing.assert_equal(uri.query_string, empty_string) - - -def test_uri_parse_https_with_port(): - var uri = URI.parse("https://example.com:8080/index.html")[URI] - testing.assert_equal(uri.scheme, "https") - testing.assert_equal(uri.host, "example.com:8080") - testing.assert_equal(uri.path, "/index.html") - testing.assert_equal(uri.__path_original, "/index.html") - testing.assert_equal(uri.request_uri, "/index.html") - testing.assert_equal(uri.is_https(), True) - testing.assert_equal(uri.is_http(), False) - testing.assert_equal(uri.query_string, empty_string) - - -def test_uri_parse_http_with_path(): - var uri = URI.parse("http://example.com/index.html")[URI] - testing.assert_equal(uri.scheme, "http") - testing.assert_equal(uri.host, "example.com") - testing.assert_equal(uri.path, "/index.html") - testing.assert_equal(uri.__path_original, "/index.html") - testing.assert_equal(uri.request_uri, "/index.html") - testing.assert_equal(uri.is_https(), False) - testing.assert_equal(uri.is_http(), True) - testing.assert_equal(uri.query_string, empty_string) - - -def test_uri_parse_https_with_path(): - var uri = URI.parse("https://example.com/index.html")[URI] - testing.assert_equal(uri.scheme, "https") - testing.assert_equal(uri.host, "example.com") - testing.assert_equal(uri.path, "/index.html") - testing.assert_equal(uri.__path_original, "/index.html") - testing.assert_equal(uri.request_uri, "/index.html") - testing.assert_equal(uri.is_https(), True) - testing.assert_equal(uri.is_http(), False) - testing.assert_equal(uri.query_string, empty_string) - - -def test_uri_parse_http_basic(): - var uri = URI.parse("http://example.com")[URI] - testing.assert_equal(uri.scheme, "http") - testing.assert_equal(uri.host, "example.com") - testing.assert_equal(uri.path, "/") - testing.assert_equal(uri.__path_original, "/") - testing.assert_equal(uri.request_uri, "/") - testing.assert_equal(uri.query_string, empty_string) - - -def test_uri_parse_http_basic_www(): - var uri = URI.parse("http://www.example.com")[URI] - testing.assert_equal(uri.scheme, "http") - testing.assert_equal(uri.host, "www.example.com") - testing.assert_equal(uri.path, "/") - testing.assert_equal(uri.__path_original, "/") - testing.assert_equal(uri.request_uri, "/") - testing.assert_equal(uri.query_string, empty_string) - - -def test_uri_parse_http_with_query_string(): - ... - - -def test_uri_parse_http_with_hash(): - ... From 274d68fdab0ff275bf4be2ad79e3c0b56dcd3aa7 Mon Sep 17 00:00:00 2001 From: Val Date: Fri, 17 Jan 2025 20:10:36 +0100 Subject: [PATCH 09/19] pin max version --- recipes/lightbug_http/recipe.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/recipes/lightbug_http/recipe.yaml b/recipes/lightbug_http/recipe.yaml index 3d302cc5..8a37d19a 100644 --- a/recipes/lightbug_http/recipe.yaml +++ b/recipes/lightbug_http/recipe.yaml @@ -19,7 +19,7 @@ build: requirements: host: - - max + - max =24.6 run: - ${{ pin_compatible('max') }} - small_time == 0.1.6 @@ -29,6 +29,9 @@ tests: - if: unix then: - magic run test + requirements: + run: + - max =24.6 files: recipe: - tests/lightbug_http/io/test_bytes.mojo From 965dcb0f14d8f4b376c5b155e7b719a2f436a0dc Mon Sep 17 00:00:00 2001 From: Val Date: Fri, 17 Jan 2025 20:17:05 +0100 Subject: [PATCH 10/19] bump version --- recipes/lightbug_http/recipe.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/recipes/lightbug_http/recipe.yaml b/recipes/lightbug_http/recipe.yaml index 8a37d19a..e6997ad4 100644 --- a/recipes/lightbug_http/recipe.yaml +++ b/recipes/lightbug_http/recipe.yaml @@ -1,7 +1,7 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/prefix-dev/recipe-format/main/schema.json context: - version: "0.1.8" + version: "0.1.9" package: name: "lightbug_http" @@ -9,7 +9,7 @@ package: source: - git: https://github.com/saviorand/lightbug_http.git - rev: bcd2967c6177ac2c7800b0c4d51ddc0630326b28 + rev: 9f74fb8041494812b6e7b69229aef23f62f5c11e build: number: 0 From 693dc5caed7de5961b3623d5db11cddbac787b5c Mon Sep 17 00:00:00 2001 From: Val Date: Tue, 11 Feb 2025 17:47:05 +0100 Subject: [PATCH 11/19] update commit sha --- recipes/lightbug_http/recipe.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/recipes/lightbug_http/recipe.yaml b/recipes/lightbug_http/recipe.yaml index e6997ad4..3c40cbd5 100644 --- a/recipes/lightbug_http/recipe.yaml +++ b/recipes/lightbug_http/recipe.yaml @@ -1,7 +1,7 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/prefix-dev/recipe-format/main/schema.json context: - version: "0.1.9" + version: "0.1.11" package: name: "lightbug_http" @@ -9,7 +9,7 @@ package: source: - git: https://github.com/saviorand/lightbug_http.git - rev: 9f74fb8041494812b6e7b69229aef23f62f5c11e + rev: 4e8bd73c10b8b4c4034940b1975cbcaf62b2ba27 build: number: 0 @@ -22,7 +22,7 @@ requirements: - max =24.6 run: - ${{ pin_compatible('max') }} - - small_time == 0.1.6 + - small_time == 0.0.1 tests: - script: From 3ed1655e6592686575e578a9e03ca6afa74e5c2b Mon Sep 17 00:00:00 2001 From: Valentin Erokhin <37780080+saviorand@users.noreply.github.com> Date: Tue, 11 Feb 2025 20:16:05 +0100 Subject: [PATCH 12/19] Update recipe.yaml --- recipes/lightbug_http/recipe.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/recipes/lightbug_http/recipe.yaml b/recipes/lightbug_http/recipe.yaml index 3c40cbd5..f3cceaf1 100644 --- a/recipes/lightbug_http/recipe.yaml +++ b/recipes/lightbug_http/recipe.yaml @@ -32,6 +32,7 @@ tests: requirements: run: - max =24.6 + - small_time =0.0.1 files: recipe: - tests/lightbug_http/io/test_bytes.mojo From aa0184b93a934f829ff76ac64a7cf4638ef3d59e Mon Sep 17 00:00:00 2001 From: Val Date: Wed, 12 Feb 2025 19:04:35 +0100 Subject: [PATCH 13/19] remove magic run --- recipes/lightbug_http/recipe.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recipes/lightbug_http/recipe.yaml b/recipes/lightbug_http/recipe.yaml index f3cceaf1..aabe2180 100644 --- a/recipes/lightbug_http/recipe.yaml +++ b/recipes/lightbug_http/recipe.yaml @@ -28,7 +28,7 @@ tests: - script: - if: unix then: - - magic run test + - mojo test requirements: run: - max =24.6 From f3d02c2a4ec9e34e85b30b59b65e31c431fb802c Mon Sep 17 00:00:00 2001 From: Val Date: Sun, 23 Feb 2025 18:06:25 +0100 Subject: [PATCH 14/19] bump the version --- recipes/lightbug_http/recipe.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/recipes/lightbug_http/recipe.yaml b/recipes/lightbug_http/recipe.yaml index aabe2180..deec6b70 100644 --- a/recipes/lightbug_http/recipe.yaml +++ b/recipes/lightbug_http/recipe.yaml @@ -1,7 +1,7 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/prefix-dev/recipe-format/main/schema.json context: - version: "0.1.11" + version: "0.1.13" package: name: "lightbug_http" @@ -9,7 +9,7 @@ package: source: - git: https://github.com/saviorand/lightbug_http.git - rev: 4e8bd73c10b8b4c4034940b1975cbcaf62b2ba27 + rev: 80724a256fafb0fa08c5e3ed7d8f31a108f3c831 build: number: 0 From 74c41142c277e389a0b58c3ce7e463d759607c45 Mon Sep 17 00:00:00 2001 From: Val Date: Wed, 26 Feb 2025 18:59:57 +0100 Subject: [PATCH 15/19] fix max version --- recipes/lightbug_http/recipe.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/recipes/lightbug_http/recipe.yaml b/recipes/lightbug_http/recipe.yaml index deec6b70..4a260f92 100644 --- a/recipes/lightbug_http/recipe.yaml +++ b/recipes/lightbug_http/recipe.yaml @@ -19,10 +19,10 @@ build: requirements: host: - - max =24.6 + - max =25.1 run: - ${{ pin_compatible('max') }} - - small_time == 0.0.1 + - small_time == 0.0.3 tests: - script: @@ -31,8 +31,8 @@ tests: - mojo test requirements: run: - - max =24.6 - - small_time =0.0.1 + - max =25.1 + - small_time =0.0.3 files: recipe: - tests/lightbug_http/io/test_bytes.mojo From e9c80a0b15c2dba6666ad2455081c0a23cb2c7db Mon Sep 17 00:00:00 2001 From: Val Date: Sun, 2 Mar 2025 17:35:00 +0100 Subject: [PATCH 16/19] bump small time version --- recipes/lightbug_http/recipe.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/recipes/lightbug_http/recipe.yaml b/recipes/lightbug_http/recipe.yaml index 4a260f92..5a22e416 100644 --- a/recipes/lightbug_http/recipe.yaml +++ b/recipes/lightbug_http/recipe.yaml @@ -1,7 +1,7 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/prefix-dev/recipe-format/main/schema.json context: - version: "0.1.13" + version: "0.1.15" package: name: "lightbug_http" @@ -9,7 +9,7 @@ package: source: - git: https://github.com/saviorand/lightbug_http.git - rev: 80724a256fafb0fa08c5e3ed7d8f31a108f3c831 + rev: 20f8e7e0d3aa419d45880ee243e30cda6fa786eb build: number: 0 @@ -22,7 +22,7 @@ requirements: - max =25.1 run: - ${{ pin_compatible('max') }} - - small_time == 0.0.3 + - small_time == 25.1.0 tests: - script: @@ -32,7 +32,7 @@ tests: requirements: run: - max =25.1 - - small_time =0.0.3 + - small_time =25.1.0 files: recipe: - tests/lightbug_http/io/test_bytes.mojo From a3b8c1ee382708739a6393516a6e7f231bcad19b Mon Sep 17 00:00:00 2001 From: Val Date: Thu, 10 Apr 2025 20:47:24 +0200 Subject: [PATCH 17/19] update to latest release --- recipes/lightbug_http/recipe.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/recipes/lightbug_http/recipe.yaml b/recipes/lightbug_http/recipe.yaml index 5a22e416..4de5ca4a 100644 --- a/recipes/lightbug_http/recipe.yaml +++ b/recipes/lightbug_http/recipe.yaml @@ -1,7 +1,7 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/prefix-dev/recipe-format/main/schema.json context: - version: "0.1.15" + version: "0.1.17" package: name: "lightbug_http" @@ -9,7 +9,7 @@ package: source: - git: https://github.com/saviorand/lightbug_http.git - rev: 20f8e7e0d3aa419d45880ee243e30cda6fa786eb + rev: 1d8a75c6f567d5b0de7f5419aeac209c4d1724f8 build: number: 0 @@ -19,10 +19,10 @@ build: requirements: host: - - max =25.1 + - max =25.2 run: - ${{ pin_compatible('max') }} - - small_time == 25.1.0 + - small_time == 25.2.0 tests: - script: @@ -31,8 +31,8 @@ tests: - mojo test requirements: run: - - max =25.1 - - small_time =25.1.0 + - max =25.2 + - small_time =25.2.0 files: recipe: - tests/lightbug_http/io/test_bytes.mojo From 606d3aeed67ff1012b8789c8587921788aa036c5 Mon Sep 17 00:00:00 2001 From: Val Date: Tue, 22 Apr 2025 20:16:44 +0200 Subject: [PATCH 18/19] add small_time to external --- recipes/lightbug_http/recipe.yaml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/recipes/lightbug_http/recipe.yaml b/recipes/lightbug_http/recipe.yaml index 4de5ca4a..3b6cb734 100644 --- a/recipes/lightbug_http/recipe.yaml +++ b/recipes/lightbug_http/recipe.yaml @@ -1,7 +1,7 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/prefix-dev/recipe-format/main/schema.json context: - version: "0.1.17" + version: "0.1.18" package: name: "lightbug_http" @@ -9,7 +9,7 @@ package: source: - git: https://github.com/saviorand/lightbug_http.git - rev: 1d8a75c6f567d5b0de7f5419aeac209c4d1724f8 + rev: 0ebd3287a923519c887f59496ced6fdb804e6889 build: number: 0 @@ -22,7 +22,6 @@ requirements: - max =25.2 run: - ${{ pin_compatible('max') }} - - small_time == 25.2.0 tests: - script: @@ -32,7 +31,6 @@ tests: requirements: run: - max =25.2 - - small_time =25.2.0 files: recipe: - tests/lightbug_http/io/test_bytes.mojo From 20909d202e83ab55ae205301acc5167535f0647f Mon Sep 17 00:00:00 2001 From: Val Date: Tue, 22 Apr 2025 21:15:00 +0200 Subject: [PATCH 19/19] bump to 0.1.19 --- recipes/lightbug_http/recipe.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/recipes/lightbug_http/recipe.yaml b/recipes/lightbug_http/recipe.yaml index 3b6cb734..a625b546 100644 --- a/recipes/lightbug_http/recipe.yaml +++ b/recipes/lightbug_http/recipe.yaml @@ -1,7 +1,7 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/prefix-dev/recipe-format/main/schema.json context: - version: "0.1.18" + version: "0.1.19" package: name: "lightbug_http" @@ -9,7 +9,7 @@ package: source: - git: https://github.com/saviorand/lightbug_http.git - rev: 0ebd3287a923519c887f59496ced6fdb804e6889 + rev: 4487b71ccbcfd30ace2db5d6ed975f94509e5a24 build: number: 0