Skip to content

Commit

Permalink
Add Features for Easy Web App Creation
Browse files Browse the repository at this point in the history
    Restructure Potcake for easy web app creation. Web apps can be 
    created through two new objects. WebApp and WebAppSettings. Through 
    them we provide the following features:
      - Django-style routing
      - Static file serving (with collection of files from multiple 
        directories)
      - Django-style URL reversing
      - Middleware
      - Settings management

    Details:

    Revamp URL dispatching to provide an ergonomic API free of 
    templates. Path parsing is now done at runtime which frees us from 
    the struggles templates bring. Templates are great but use of them 
    for central data structures forces user code to be templated. It 
    also makes some API structures impossible to right. For example, 
    with templates the user cannot store configuration as data prior to 
    instantiating the WebApp if the configuration uses templates that 
    need the WebApp. Also, each instantiation is a new type if the 
    template arguments differ. This prevents storing template 
    instantiations in a collection. We make heavy use of delegates for 
    storing code to use an object instead of objects themselves. This is
    handy for storing disparate objects in a collection when they all 
    are alike in the actions carried out using them.
     
    Add the ability to obtain a route path by supplying a pre-registered
    name and path arguments (reversing). The reversing function makes 
    use of a new reference to the initialized app at runtime.
    
    Add toPath functions to path converters for converting path 
    arguments to their string representations in paths. Return a common 
    exception from path converters to aid signaling when a reversed path
    cannot be created. Use Variant instead of emplacing and cast for 
    easier-to-use code.
    
    Rehash Router associative arrays for performance.
    
    Add static file handling via two approaches:
      - Single-directory approach where all files are stored in a single
        directory in development and production.
      - Collecting approach where static files are merged into one 
        directory and served from a single routh path.

    Add `staticPath` method for building static paths in templates.

    Add WebApp.addRoutes for adding all routes at once using a 
    collection. Make a trailing '/' optional when calling a route if the
    route's path was specified with a trailing '/'
    
    Streamline static file serving for the single directory case. The 
    developer can configure serving via the settings object and all 
    static serving for all cases is now done via serveStaticFiles.

    Wrap all modules in @safe.
  • Loading branch information
kyleingraham committed Jul 14, 2023
1 parent 02638c6 commit 7a729d8
Show file tree
Hide file tree
Showing 21 changed files with 1,460 additions and 306 deletions.
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Development artifacts
.dub
dub.selections.json
docs.json
Expand All @@ -8,11 +9,21 @@ libpotcake*.dylib
libpotcake*.dll
libpotcake*.a
libpotcake*.lib
potcake*.lib
potcake-test-*
*.exe
*.o
*.obj
*.lst
*.pdb
*.dll

# IDE project files
.idea
*.iml

# Project examples
*-example

# OS files
.DS_Store
227 changes: 180 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,56 @@
# Potcake
An easy to live with, sensible, and dependable web framework built on [vibe.d](https://vibed.org/).

Potcake endeavours to:
- Provide safe and sane defaults
- Be easy to use how you want or with our opinionated structure
- Provide useful components that prevent re-inventing the wheel

## URL Dispatching
## Examples
If you would like to see Potcake in action before reading further, take a look at the [examples](examples) folder. There
you will find demonstration apps for Potcake's features.

Each can be run by cloning this repo, navigating to the example app's base, and running `dub run`
(on macOS `MACOSX_DEPLOYMENT_TARGET=11 dub run`).

[collect_static_files](examples/collect_static_files)

Demonstrates:
- Adding multiple route handlers at once and static file serving.
- Specifying typed URL handlers.
- Collection of static files from multiple source directories into a single runtime directory using the
relevant settings (`staticDirectories`, `rootStaticDirectory`, `staticRoutePath`) and `--collectstatic` file collection.
- Use of `staticPath` for linking to files in the runtime static directory for both inline and DIET templates.

[static_files](examples/static_files)

Demonstrates:
- Adding route handlers and static file serving.
- Serving static files from a single directory.

## Web Framework

### URL Dispatching
Potcake implements [Django's](https://www.djangoproject.com/) URL dispatching system.

```d
import potcake;
@safe:
import vibe.core.core : runApplication;
import vibe.http.server : HTTPServerSettings, listenHTTP;
import vibe.http.status : HTTPStatus;
import potcake.web;
int main()
{
auto router = new Router!();
router.get!"/hello/<name>/<int:age>/"(&helloUser);
auto settings = new HTTPServerSettings;
settings.bindAddresses = ["127.0.0.1"];
settings.port = 9000;
auto listener = listenHTTP(settings, router);
scope (exit)
listener.stopListening();
auto webApp = new WebApp;
webApp
.addRoute("/hello/<name>/<int:age>/", &helloUser);
return runApplication();
return webApp.run();
}
void helloUser(HTTPServerRequest req, HTTPServerResponse res, string name, int age) @safe
void helloUser(HTTPServerRequest req, HTTPServerResponse res, string name, int age)
{
import std.conv : to;
import vibe.http.status : HTTPStatus;
res.contentType = "text/html; charset=UTF-8";
res.writeBody(`
Expand All @@ -45,9 +65,8 @@ void helloUser(HTTPServerRequest req, HTTPServerResponse res, string name, int a
}
```

### Details
Potcake uses D's flexibility to implement key components of Django's URL dispatching system. The end result is a
blending of the ergonomics available in Django with the, to me, superior development experience of D.
blending of the ergonomics available in Django with the superior runtime performance of D.

Key components of Django's [URL dispatching system](https://docs.djangoproject.com/en/dev/topics/http/urls/#url-dispatcher) are:
- The URL path expression scheme
Expand Down Expand Up @@ -83,53 +102,51 @@ Behind the scenes, path converters are objects that:
- Understand how to convert string values to the path converter's return type

#### potcake.http.router.Router
Potcake provides `Router` which is a vibe.d router that understands Django's URL path expression scheme.
Paths are parsed at compile-time using built-in or user-provided path converters. Built-in path converters match
Potcake provides `Router` (used internally by `WebApp`) which is a vibe.d router that understands Django's URL path expression scheme.
Paths are parsed at run-time using built-in or user-provided path converters. Built-in path converters match
[Django's built-in set](https://docs.djangoproject.com/en/dev/topics/http/urls/#path-converters). User-specified path
converters must first be defined as structs with the following properties:
converters must first be defined as `@safe` structs with the following properties:

- An `enum` member named `regex` with a regex character class representing strings to match against within a requested path.
- A `@safe` `toD` function that accepts a `const string`. The return type can be any desired outside `void`. This function converts strings to the type produced by the path converter.
- A `toD` function that accepts a `const string`. The return type can be any desired outside `void`. This function converts strings to the type produced by the path converter.
- A `toPath` function with a parameter of the path converters type that returns a `string`.

##### User-defined Path Converter Example

```d
import potcake;
import potcake.web;
import vibe.core.core : runApplication;
import vibe.http.server : HTTPServerSettings, listenHTTP;
import vibe.http.status : HTTPStatus;
struct NoNinesIntConverter
@safe struct NoNinesIntConverter
{
import std.conv : to;
enum regex = "[0-8]+"; // Ignores '9'
int toD(const string value) @safe
int toD(const string value)
{
import std.conv : to;
// Overflow handling left out for simplicity.
return to!int(value);
}
string toPath(int value)
{
return to!string(value);
}
}
int main()
{
auto router = new Router!([bindPathConverter!(NoNinesIntConverter, "nonines")]);
router.get!"/hello/<name>/<nonines:age>/"(&helloUser);
auto settings = new HTTPServerSettings;
settings.bindAddresses = ["127.0.0.1"];
settings.port = 9000;
auto webApp = new WebApp([
pathConverter("nonines", NoNinesIntConverter())
])
.addRoute("/hello/<name>/<nonines:age>/", &helloUser);
auto listener = listenHTTP(settings, router);
scope (exit)
listener.stopListening();
return runApplication();
return webApp.run();
}
void helloUser(HTTPServerRequest req, HTTPServerResponse res, string name, int age) @safe {
import std.conv : to;
import vibe.http.status : HTTPStatus;
res.contentType = "text/html; charset=UTF-8";
res.writeBody(`
Expand All @@ -145,13 +162,130 @@ void helloUser(HTTPServerRequest req, HTTPServerResponse res, string name, int a
```

##### Handlers
Handlers given to `Router` (like with `URLRouter`) should at the very least return `void` and accept an
Handlers given to `Router` via `WebApp` should at the very least return `void` and accept an
`HTTPServerRequest` and an `HTTPServerResponse`. Values extracted from the request's path are saved to
`HTTPServerRequest.params` as strings.

If the parameter signature for a handler is extended with the types returned by its path's path converters then
`Router` will additionally use the path converters' `toD` functions to pass converted values to the handler.

### URL Reversing
Potcake provides a utility function for producing absolute URL paths at `potcake.web.reverse`. It allows you to define
route path specifics in one place while using those paths anywhere in your code base. If you make a change to the central
definition, all usages of that definition will be updated.

For example, given the following route definition:

```d
webApp.addRoute("/hello/<name>/<int:age>/", &helloUser, "hello");
```

you can reverse the definition like so:

```d
reverse("hello", "Potcake", 2);
```

producing the following URL path:

```d
"/hello/Potcake/2/"
```

### Static Files
Potcake offers two ways to organize your static files (e.g. images, JavaScript, CSS):
1. In a central directory
2. In multiple directories e.g. a directory per package in a larger project.

#### Central Directory
1. Choose one directory in your project for storing all static files.
2. Set `WebAppSettings.rootStaticDirectory` to the relative path to your static directory from your compiled executable. You will need to deploy this directory alongside your executable.
3. Set `WebAppSettings.staticRoutePath` to the URL to use when referring to static files e.g. "/static/".
4. Use `potcake.web.staticPath` to build URLs for static assets.
5. Call `WebApp.serveStaticfiles()` before running your app.

See [static_files](examples/static_files) for a demonstration.

#### Multiple directories
1. Store your static files in directories throughout your project (in the future we hope to use this to make it possible to build libraries that carry their own static files and templates).
2. Set `WebAppSettings.staticDirectories` to the relative paths to your static directories from your compiled executable.
3. Set `WebAppSettings.rootStaticDirectory` to directory that all of your static files should be collected. You will need to deploy this directory alongside your executable. When files are collected we use a merge strategy. In the future we hope to use this to make it easy to overwrite a library's static files with your own.
4. Set `WebAppSettings.staticRoutePath` to the URL to use when referring to static files e.g. "/static/".
5. Use `potcake.web.staticPath` to build URLs for static assets.
6. Call `WebApp.serveStaticfiles()` before running your app.
7. Add the following lines to your dub file and setup your main entry point function to accept program arguments. Pass these arguments to `WebApp.run`.
```d
postBuildCommands "\"$DUB_TARGET_PATH\\$DUB_ROOT_PACKAGE_TARGET_NAME\" --collectstatic" platform="windows"
postBuildCommands "\"$DUB_TARGET_PATH/$DUB_ROOT_PACKAGE_TARGET_NAME\" --collectstatic" platform="posix"
```

See [collect_static_files](examples/collect_static_files) for a demonstration.

### Middleware
Potcake provides a framework for middleware. Any middleware provided is run after a request has been routed.

To add middleware, create a `MiddlewareDelegate`. A `MiddlewareDelegate` should accept and call a
`HTTPServerRequestDelegate` that represents the next middleware in the chain. A middleware can carry out actions both
before and after the next middleware has been called. A `MiddlewareDelegate` should return a
`HTTPServerRequestDelegate` that can be called by the middleware prior to it.

For example:

```d
import potcake.web;
HTTPServerRequestDelegate middleware(HTTPServerRequestDelegate next)
{
void middlewareDelegate(HTTPServerRequest req, HTTPServerResponse res)
{
// Run actions prior to the next middleware.
next(req, res);
// Run actions after the next middleware.
}
return &middlewareDelegate;
}
int main()
{
auto webApp = new WebApp;
webApp
.addRoute("/", delegate void(HTTPServerRequest req, HTTPServerResponse res) {})
.addMiddleware(&middleware);
return webApp.run();
}
```

## Settings
On initial setup, a Potcake `WebApp` accepts a settings class in the family of `WebAppSettings`. `WebAppSettings` has
settings core to Potcake with the framework's defaults.

You can add your own settings by subclassing `WebAppSettings` and adding your own fields.

Potcake provides a way to access your settings at runtime from anywhere in your program in `getSetting`.

For example:

```d
import potcake.web;
class MySettings : WebAppSettings
{
string mySetting = "my setting";
}
int main()
{
auto webApp = new WebApp(new MySettings);
webApp
.addRoute("/", delegate void(HTTPServerRequest req, HTTPServerResponse res) {
auto setting = (() @trusted => getSetting("mySetting").get!string)();
});
return webApp.run();
}
```

## FAQ
Q - Why the name Potcake?
Expand All @@ -164,11 +298,10 @@ and dependable. All great aspirational qualities for a web framework.
- Middleware
- [x] Middleware system
- Convenience middleware
- [ ] Static files
- [x] Static files
- [ ] CORS
- [ ] Post-routing middleware
- [x] Post-routing middleware
- [ ] Health-check endpoint middleware
- [ ] Class-based middleware
- Matching the API for vibe.d's `URLRouter`
- [ ] Set of valid handler signatures
- [ ] Handler registration functions e.g. `post`
Expand Down
30 changes: 24 additions & 6 deletions dub.sdl
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,22 @@ copyright "Copyright © 2022, Kyle Ingraham"
license "MIT"

dependency "potcake:http" version="*"
dependency "potcake:web" version="*"

targetType "library"
targetName "potcake"

sourcePaths "."
importPaths "."
sourcePaths "http" "web"
importPaths "http" "web"

subPackage {
name "http"
description "Potcake web framework lower-level http components. Includes its vibe.d router."

dependency "pegged" version="~>0.4.6"
dependency "vibe-core" version="~>1.22.4"
dependency "vibe-d:http" version="~>0.9.5"
dependency "vibe-d:inet" version="~>0.9.5"
dependency "vibe-core" version="~>2.2.0"
dependency "vibe-d:http" version="~>0.9.6"
dependency "vibe-d:inet" version="~>0.9.6"

targetType "library"

Expand All @@ -28,4 +29,21 @@ subPackage {

lflags "-L/opt/local/lib/openssl-3" platform="osx" // Location used by MacPorts for openssl3
lflags "-L/opt/local/lib/openssl-1.1" platform="osx" // Location used by MacPorts for openssl11
}
}

subPackage {
name "web"
description "Potcake web framework higher-level web app components."

dependency "diet-ng" version="~>1.8.1"
dependency "potcake:http" version="*"
dependency "unit-threaded:assertions" version="~>2.1.6"
dependency "urllibparse" version="~>0.1.0"

targetType "library"

sourcePaths "web"
importPaths "web"

dflags "-J \".\"" // Allows the user to specify template locations in code
}
7 changes: 7 additions & 0 deletions examples/collect_static_files/dub.sdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name "collect-static-files-example"
description "Demonstrates a web app that serves collected static files."

dependency "potcake:web" path="../../"

postBuildCommands "\"$DUB_TARGET_PATH\\$DUB_ROOT_PACKAGE_TARGET_NAME\" --collectstatic" platform="windows"
postBuildCommands "\"$DUB_TARGET_PATH/$DUB_ROOT_PACKAGE_TARGET_NAME\" --collectstatic" platform="posix"
Loading

0 comments on commit 7a729d8

Please sign in to comment.