Coder Social home page Coder Social logo

rawleyfowler / humming-bird Goto Github PK

View Code? Open in Web Editor NEW
42.0 1.0 7.0 238 KB

A fun and performant web application framework for Raku

License: MIT License

Raku 99.94% CSS 0.06%
http http-server raku web-framework webapp api-rest rest hacktoberfest

humming-bird's Introduction

Humming-Bird

Zef Badge SparrowCI

Humming-Bird is a simple, composable, and performant web-framework for Raku on MoarVM. Humming-Bird was inspired mainly by Sinatra, and Express, and tries to keep things minimal, allowing the user to pull in things like templating engines, and ORM's on their own terms.

Features

Humming-Bird has 2 simple layers, at the lowest levels we have Humming-Bird::Glue which is a simple "glue-like" layer for interfacing with Humming-Bird::Backend's. Then you have the actual application logic in Humming-Bird::Core that handles: routing, middleware, error handling, cookies, etc.

  • Powerful function composition based routing and application logic

    • Routers
    • Groups
    • Middleware
    • Advice (end of stack middleware)
    • Simple global error handling
    • Plugin system
  • Simple and helpful API

    • get, post, put, patch, delete, etc
    • Request content will be converted to a Raku Hash if possible
    • Static files served have their content type infered
    • Request/Response stash's for inter-layer route talking
  • Swappable backends

Note: Humming-Bird is not meant to face the internet directly. Please use a reverse proxy such as Apache, Caddy or NGiNX.

How to install

Make sure you have zef installed.

Install latest

zef -v install https://github.com/rawleyfowler/Humming-Bird.git

Install stable

zef install Humming-Bird

Performance

See this for a more detailed performance preview vs. Ruby's Sinatra using Humming-Bird::Backend::HTTPServer.

Examples

Simple example:

use v6.d;

use Humming-Bird::Core;

get('/', -> $request, $response {
    $response.html('<h1>Hello World</h1>');
});

listen(8080);

# Navigate to localhost:8080!

Simple JSON CRUD example:

use v6.d;

use Humming-Bird::Core;
use JSON::Fast; # A dependency of Humming-Bird

my %users = Map.new('bob', %('name', 'bob'), 'joe', %('name', 'joe'));

get('/users/:user', -> $request, $response {
    my $user = $request.param('user');

    if %users{$user}:exists {
        $response.json(to-json %users{$user});
    } else {
        $response.status(404).html("Sorry, $user does not exist.");
    }
});

post('/users', -> $request, $response {
    my %user = $request.content; # Different from $request.body, $request.content will do its best to decode the data to a Map.
    if my-user-validation-logic(%user) { # Validate somehow, i'll leave that up to you.
        %users{%user<name>} = %user;
        $response.status(201); # 201 created
    } else {
        $response.status(400).html('Bad request');
    }
});

listen(8080);

Using plugins

use v6.d;

use Humming-Bird::Core;

plugin 'Logger'; # Corresponds to the pre-built Humming-Bird::Plugin::Logger plugin.
plugin 'Config'; # Corresponds to the pre-built Humming-Bird::Plugin::Config plugin.

get('/', sub ($request, $response) {
    my $database_url = $request.config<database_url>;
    $response.html("Here's my database url :D " ~ $database_url);
});

listen(8080);

Routers

use v6.d;

use Humming-Bird::Core;
use Humming-Bird::Middleware;

# NOTE: Declared routes persist through multiple 'use Humming-Bird::Core' statements
# allowing you to declare routing logic in multiple places if you want. This is true
# regardless of whether you're using the sub or Router process for defining routes.
my $router = Router.new(root => '/');

plugin 'Logger';

$router.get(-> $request, $response { # Register a GET route on the root of the router
    $response.html('<h1>Hello World</h1>');
});

$router.get('/foo', -> $request, $response { # Register a GET route on /foo
    $response.html('<span style="color: blue;">Bar</span>');
});

my $other-router = Router.new(root => '/bar');

$other-router.get('/baz', -> $request, $response { # Register a GET route on /bar/baz
    $response.file('hello-world.html'); # Will render hello-world.html and infer its content type
});

# No need to register routers, it's underlying routes are registered with Humming-Bird on creation.
listen(8080); 

Middleware

use v6.d;

use Humming-Bird::Core;
use Humming-Bird::Middleware;

get('/logged', -> $request, $response {
    $response.html('This request has been logged!');
}, [ &middleware-logger ]); # &middleware-logger is provided by Humming-Bird::Middleware

# Custom middleware
sub block-firefox($request, $response, &next) {
    return $response.status(400) if $request.header('User-Agent').starts-with('Mozilla');
    $response.status(200);
}

get('/no-firefox', -> $request, $response {
    $response.html('You are not using Firefox!');
}, [ &middleware-logger, &block-firefox ]);

listen(8080);

Since Humming-Bird 3.0.4 it may be more favorable to use plugins to register global middlewares.

Swappable Backends

use v6.d;

use Humming-Bird::Core;

get('/', -> $request, $response {
    $response.html('This request has been logged!');
});

# Run on a different backend, assuming: 
listen(:backend(Humming-Bird::Backend::MyBackend));

More examples can be found in the examples directory.

Swappable backends

In Humming-Bird 3.0.0 and up you are able to write your own backend, please follow the API outlined by the Humming-Bird::Backend role, and view Humming-Bird::Backend::HTTPServer for an example of how to implement a Humming-Bird backend.

Plugin system

Humming-Bird 3.0.4 and up features the Humming-Bird Plugin system, this is a straight forward way to extend Humming-Bird with desired functionality before the server starts up. All you need to do is create a class that inherits from Humming-Bird::Plugin, for instance Humming-Bird::Plugin::OAuth2, expose a single method register which takes arguments that align with the arguments specified in Humming-Bird::Plugin.register, for more arguments, take a slurpy at the end of your register method.

Here is an example of a plugin:

use MONKEY-TYPING;
use JSON::Fast;
use Humming-Bird::Plugin;
use Humming-Bird::Core;

unit class Humming-Bird::Plugin::Config does Humming-Bird::Plugin;

method register($server, %routes, @middleware, @advice, **@args) {
    my $filename = @args[0] // '.humming-bird.json';
    my %config = from-json($filename.IO.slurp // '{}');

    augment class Humming-Bird::Glue::HTTPAction {
        method config(--> Hash:D) {
            %config;
        }
    }

    CATCH {
        default {
            warn 'Failed to find or parse your ".humming-bird.json" configuration. Ensure your file is well formed, and does exist.';
        }
    }
}

This plugin embeds a .config method on the base class for Humming-Bird's Request and Response classes, allowing your config to be accessed during the request/response lifecycle.

Then to register it in a Humming-Bird application:

use Humming-Bird::Core;

plugin 'Config', 'config/humming-bird.json'; # Second arg will be pushed to he **@args array in the register method.

get('/', sub ($request, $response) {
    $response.write($request.config<foo>); # Echo back the <foo> field in our JSON config.
});

listen(8080);

Design

  • Humming-Bird should be easy to pickup, and simple for developers new to Raku and/or web development.
  • Humming-Bird is not designed to be exposed to the internet directly. You should hide Humming-Bird behind a reverse-proxy like NGiNX, Apache, or Caddy.
  • Simple and composable via middlewares.

Things to keep in mind

  • This project is in active development, things will break.
  • You may run into bugs.
  • This project is largely maintained by one person.

Contributing

All contributions are encouraged! I know the Raku community is amazing, so I hope to see some people get involved :D

Please make sure you squash your branch, and name it accordingly before it gets merged!

Testing

Install App::Prove6

zef install --force-install App::Prove6

Ensure that the following passes:

cd Humming-Bird
zef install . --force-install --/test
prove6 -v -I. t/ it/

License

Humming-Bird is available under the MIT, you can view the license in the LICENSE file at the root of the project. For more information about the MIT, please click here.

humming-bird's People

Contributors

drudgesentinel avatar masukomi avatar melezhik avatar rawleyfowler avatar swaggboi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

humming-bird's Issues

Global error handler/error handling middleware

Right now if the program dies, it dies. We should be able to catch these errors and handle them based on how the user of the library wants. Note the internal code will not die, but rather in the handlers of the defined routes. Ideally the errors in routing code (ie. userspace code), will be caught and translated into a 500 of some kind.

Response should know about it's corresponding request

Right now, the flow of information is very one way. This makes it hard to audit, log, etc. I think Responses should at the very least know the path of the request that triggered it's generation. This will greatly improve the usability of advice.

Add scopes

Right now you have to apply middleware individually to each route. We should have some sort of scoping/grouping mechanism that injects given middleware into children route declarations.

multipart/form-data POST body doesn't parse

Hello, trying to figure out how to handle a file upload via multipart/form-data POST body; here is my Humming-Bird code:

#!/usr/bin/env raku

use v6.d;
use Humming-Bird::Core;
use Humming-Bird::Middleware;
use Humming-Bird::Advice;
use Template::Mustache;

my $template = Template::Mustache.new: :from<./templates>;

middleware &middleware-logger;
advice     &advice-logger;

get '/', -> $request, $response {
    my Str %stash = title => 'File upload';

    $response.html($template.render: 'index', %stash);
};

post '/', -> $request, $response {
    dd $request;

    $response.redirect('/');
};

listen 3000;

And here is my template/form:

<!DOCTYPE html>
<html>
<head>
  <title>{{title}}</title>
</head>
<body>
<h1>{{title}}</h1>
<form method="post" enctype="multipart/form-data">
  <div>
    <label for="file-upload">File</label>
    <input type="file" name="file-upload" id="file-upload"
           accept="image/jpeg, image/png, .jpeg, .jpg, .png">
  </div>
  <button type="submit">Upload</button>
</form>
</body>
</html>

When I make the POST in my browser by uploading a small (<100KB) JPEG, I get a TCP reset in the browser and in the text console I get:

Malformed UTF-8 near bytes 0d 0a ff at line 23 col 1
  in block  at /home/daniel/.raku/sources/000B68E7460EE82EB3F0D7FFB96DAB6C0199C762 (Humming-Bird::HTTPServer) line 43

I get this even if I comment out the dd $request line so it seems to not like that POST body. I tried stopping the script after performing the GET to render the form, firing up nc -l 3000 in a terminal and then completing the form in my browser. The POST request body includes a Content-Disposition header and a Content-Type header and looks like a well-formed multipart/form-data POST to me:

POST / HTTP/1.1
Host: localhost:3000
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115
.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: multipart/form-data; boundary=---------------------------21740014396030127931917243238
Content-Length: 31382
Origin: http://localhost:3000
DNT: 1
Connection: keep-alive
Referer: http://localhost:3000/
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: same-origin
Sec-Fetch-User: ?1

-----------------------------21740014396030127931917243238
Content-Disposition: form-data; name="file-upload"; filename="screenboop.jpg"
Content-Type: image/jpeg

<binary stuff here...>
-----------------------------21740014396030127931917243238--

I was hoping I could access the content-type and parameter name attributes in addition to the bytes that comprise the file itself. Would be neat to handle multiple files too using the filename attribute if that's do-able?

Perform some benchmarks/optimizations

I think overall, Humming-Bird is pretty fast. But I'm sure it can perform better. I think it would be a good idea to get a performance comparison between Humming-Bird and other web-frameworks (not necessarily just Raku frameworks), then see where we can improve performance from there.

Investigate trie based routing

We may be able to speed up routing with a trie based routing approach similar to Opium. It's probably worth looking into.

Stash for templates?

Hello I'm trying to populate a template with something that the user submits in a POST body:

use Humming-Bird::Core;
use Template6;

my $templates = Template6.new;
$templates.add-path: 'templates';

my $router = Router.new(root => '/');

$router.post(-> $request, $response {
    my $output-value = $request.content.{'input-value'};

    $response.html($templates.process: 'index', :$output-value);
});

$router.get(-> $request, $response {
    $response.html($templates.process: 'index');
});

The template can just be:

<h1>Cool template</h1>
[% IF output-value %]
<p>Output value is: [% output-value %]</p>
[% END %]

The problem I'm having is that 'output-value' lives on until someone submits another. I expected it to just exist for that one request/response transaction. I see there's a 'stash' in the Middleware class; is this something I can or should use to pass this value to the template and then be 'forgotten' after the transaction?

Add middleware

The next big step for this project is to implement middleware. A simple and naive way to do it would be to include a List of functions that type Request --> Request as a parameter on the get, post, put ... functions. Then all we have to do is compose all of the functions together and use their result as the input for the handler. But I am open to other implementations.

Add simpler body decoding

Right now if I want to decode URL-encoded data, or JSON data I need to grab the body, then perform the operation. I would prefer to be able to just do .content on request and have the library infer the type and perform the conversion.

Add express style routers

I think it would be nice to be able to construct routers, this would be an OO style route declaration, that would provide the basis for more simpler grouping, and separation of concerns.

Add static content routing

There should be a simple way to specify a route/location as static and have it source files from a directory.

Something like:

use Humming-Bird::Core;

static('/static', IO::Path:D);

Headers are case-sensitive

According to the HTTP spec, headers are case-insensitive.

For example, currently requests from Cro::HTTP::Client to a Humming-Bird server will fail due to case-sensitivity.

filenames aren't parsed correctly in Glue

Hello!
I'm noodling around the framework, and I've found behavior that I think is unintended. I wanted to get some clarification on how this should look to make sure I'm understanding the issue correctly and would like to submit a PR to sort it.

The issue lies here:
https://github.com/rawleyfowler/Humming-Bird/blob/35ca50b641a878209d7a6b1f73fe4bbfe2d118c1/lib/Humming-Bird/Glue.rakumod#L188C25-L196C22

The naming syntax shortens the filename, and also removes the extension. Here's a trivial example using the same regex:

my $example-file = "name=\"pexels-photo-132204.jpg\"";
my $name = $example-file.match(/'name="'<(\w+)>'";'?.*/).Str;
say $name; #pexels

It looks like both the name (from whatever template or markdown) and filename properties are both affected.

So what I wanted to verify is-

  1. Is it intended to retain the file extension in filename (I'm assuming yes, but I'm noob so wanted to clarify
  2. I'm happy to provide a more specific regex in PR form that would address the desired formatting of that property

Let me know if you'd like additional clarification, or if my example doesn't make sense.

Thanks!

Add session system

Right now there is no "nice" session system, instead you have to roll your own with middlewares. It would be nice to have a unified API for this.

Swappable back-ends

@tony-o Has brought up the idea of having "swappable" backends for Humming-Bird, this would be similar to Puma, Unicorn, etc, for Ruby.

This will serve as a thread for ideas, etc. Regarding it's implementation.

WebSockets

One of the last HTTP features Humming-Bird is missing is WebSockets.

Blob file types cannot be served

Because I naively encoded response bodies as strings originally, it would be best if we moved to Buf's so that we can allow for simpler transmission of blob file types (images, pdf, etc..)

POST body parsing fails

I have the following route to process a POST form submission:

$router.post(-> $request, $response {
    my Str $return-url = $request.content.{'hyperlink'};
    my Str $url-scheme = $request.headers.{'X-Forwarded-Proto'} || 'http';
    my Str $url-host   = $request.headers.{'Host'};
    my Str $base-url   = $url-scheme ~ '://' ~ $url-host ~ '/';
    my Str $hyperlink  = $base-url ~ encode-base64(gzip($return-url), :str);

    $response.html($templates.process: 'index', :$hyperlink);
});

This seems to work but today I noticed there's a couple strings that it won't accept, for example if I submit https://youtube.com/watch?v=xvFZjo5PgG0 I get the following in my console:

[Context: 91141] | [Time: 2023-10-31T21:51:07.598590-04:00] | POST | / | HTTP/1.1 | Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Failed trying to parse a body of type application/x-www-form-urlencoded
  in block  at /home/daniel/.raku/sources/1F9F48EAAE7A585D01855E91A4F8E592CF5AB1E1 (Humming-Bird::Core) line 123 
[Context: 91141] | [Time: 2023-10-31T21:51:07.628344-04:00] | 500 Internal Server Error | / | text/html

I thought maybe the question mark/query string confused it but I've tried other URLs with query strings and they work, it's just this link and https://youtube.com/watch?v=oHg5SJYRHA0 it doesn't seem to like so far.

Handle absolute URI's

Right now the library cannot handle absolute URI's in the request line. However they are part of the HTTP/1.1 spec.

Example request that needs to be handled:

GET localhost:8080/users HTTP/1.1
* headers *

Example request that we already handle:

GET /users HTTP/1.1
Host: localhost:8080
* headers *

Add catch-all routes

Right now you can't easily add a catch-all route for a given location.

For example it might be nice to do:

get('/*', ...);

So that any route that doesn't fit an already defined location can be caught.

Add Cookies

Right now there are no cookie mechanisms, let's add some!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.