Skip to content

Commit

Permalink
dynamic paths added
Browse files Browse the repository at this point in the history
  • Loading branch information
Asad Memon committed Jun 21, 2019
1 parent e83e4e5 commit 74ff544
Show file tree
Hide file tree
Showing 18 changed files with 248 additions and 57 deletions.
29 changes: 23 additions & 6 deletions docs/nodejs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,38 @@ module.exports = function(req, res) {

If you send POST request to `/submit` with `json` or `urlencoded` body. It will be parsed and populated in `req.body`.

## Route Rewrites
## Dynamic Routes (Pretty URL Slugs)

Zero decides routes based on file structure. But sometimes you would want to change `/user?id=luke` to `/user/luke`. To cater this type of routes, **all requests sent to a route that doesn't exist are passed on to the closest parent function**.
Zero decides routes based on file structure. Most projects also require dynamic routes like `/user/luke` and `/user/anakin`. Where `luke` and `anakin` are parameters. Zero natively supports this type of routes: any file or folder that **starts with \$** is considered a dynamic route.

So if you visit `/user/luke` and there is no `./user/luke.js` but there is `./user.js`. Zero will send the request to `/user` and set `req.params` to `['luke']`. Code for this:
So if you create `./user/$username.js` and then from browser visit `/user/luke`, Zero will send that request to `$username.js` file and set `req.params` to `{username: 'luke'}`. Code for this:

```js
// user.js
/*
project/
└── user/
└── $username.js <- this file
*/
module.exports = function(req, res) {
console.log(req.params); // ['luke'] when user visits /user/luke
console.log(req.params); // = {username: 'luke'} when user visits /user/luke
res.send({ params: req.params });
};
```

Another example: if you visit `/user/luke/messages`. Zero will also forward this to `./user.js` and set `req.params` to `['luke', 'messages']`
Parameters apply to folder-names too. Another example: if you want to cater `/user/luke/messages` route, you can handle this with following directory structure:

```
project/
└── user/
└── $username/
└── index.js
└── messages.js
```

- `index.js` handles `/user/:username` routes.
- `messages.js` handles `/user/:username/messages` routes.

**Tip:** `$` is used by Bash for variables. So it might be confusing when you do `cd $username` or `mkdir $username` and nothing happens. The right way to do this is escaping the `$` ie. `cd \$username` or `mkdir \$username`.

## Fetch API

Expand Down
37 changes: 37 additions & 0 deletions docs/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,43 @@ numpy==1.15.0
pandas==0.20.3
```

## Dynamic Routes (Pretty URL Slugs)

Zero decides routes based on file structure. Most projects also require dynamic routes like `/user/luke` and `/user/anakin`. Where `luke` and `anakin` are parameters. Zero natively supports this type of routes: any file or folder that **starts with \$** is considered a dynamic route.

So if you create `./user/$username.py` and then from browser visit `/user/luke`, Zero will send that request to `$username.py` file and your handler should accept a `username` argument. Code for this:

```python
# project/
# └── user/
# └── $username.js <- this file

def handler(username):
return "Hello, " + username
```

Parameters apply to folder-names too. Another example: if you want to cater `/user/luke/messages` route, you can handle this with following directory structure:

```
project/
└── user/
└── $username/
└── index.py
└── messages.py
```

- `index.py` handles `/user/:username` routes.
- `messages.py` handles `/user/:username/messages` routes.

You can also have nested dynamic paths like `/user/:username/:commentId` with a handler in `./user/$username/$commentId` with the following code:

```
def handler(username, commentId):
return username + " says: " + getComment(commentId)
```

**Tip:** `$` is used by Bash for variables. So it might be confusing when you do `cd $username` or `mkdir $username` and nothing happens. The right way to do this is escaping the `$` ie. `cd \$username` or `mkdir \$username`.

## POST Data

Here is an example of how to get POST data (sent from an HTML form) in your Python handler:
Expand Down
10 changes: 8 additions & 2 deletions packages/core/lib/builder/buildManifest.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,15 @@ async function buildManifest(buildPath, oldManifest, fileFilter) {
var trimmedPath = slash(endpoint[0]).replace(buildPath, "/");
trimmedPath = trimmedPath
.split(".")
.slice(0, -1)
.slice(0, -1) // remove extension
.join(".")
.toLowerCase(); // remove extension
// lowercase path except $paramNames
.split("/")
.map(p => {
if (!p.startsWith("$")) return p.toLowerCase();
return p;
})
.join("/");
if (trimmedPath.endsWith("/index")) {
trimmedPath = trimmedPath
.split("/index")
Expand Down
61 changes: 48 additions & 13 deletions packages/core/lib/router/matchPath.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,13 @@ function matchPathWithDictionary(

// check for partial match now ie. query is: /login/username and endpoint will be /login
// reverse sort to have closest/deepest match at [0] ie. [ "/login/abc/def", "/login/abc", "/login" ]
var matches = Manifest.lambdas
.filter(endpoint => {
return (
endpoint[2] !== "static" &&
path.startsWith(endpoint[0]) &&
// if both /user and /users lambdas exist in same directory.
path.replace(endpoint[0], "").startsWith("/")
);
})
.sort()
.reverse();
if (matches && matches[0]) {
return matches[0];
var matches = [];
Manifest.lambdas.forEach(endpoint => {
const matchedParams = matchPath(endpoint[0], path);
if (matchedParams) matches.push(endpoint);
});
if (matches.length > 0) {
return getPreferredPath(matches, path);
}
} else {
return match;
Expand All @@ -69,4 +63,45 @@ function matchPathWithDictionary(
return "404"; // not found
}

function matchPath(patternPath, givenPath) {
patternPath = patternPath.split("/").filter(a => !!a); // remove empty element
givenPath = givenPath.split("/").filter(a => !!a);

var matches = true;
var params = {};
if (givenPath.length !== patternPath.length) matches = false;
else {
givenPath.forEach((p, i) => {
if (p === patternPath[i]) return;
if (!patternPath[i]) return (matches = false);
if (patternPath[i].startsWith("$") && patternPath[i].length > 1) {
params[patternPath[i].slice(1)] = p;
return;
}
matches = false;
});
}

// if path matches, return params (if any)
if (matches) debug("matched", givenPath, patternPath, params);
if (matches) return params;
}

// when two paths match the given path, choose the one without param variable
function getPreferredPath(matches, givenPath) {
givenPath = givenPath.split("/").filter(a => !!a);
var chosen = matches[0];

if (matches.length > 1) {
console.log(matches);
matches.forEach(endpointData => {
const patternPath = endpointData[0].split("/").filter(a => !!a);
if (patternPath && !patternPath[patternPath.length - 1].startsWith("$")) {
chosen = endpointData;
}
});
}
return chosen;
}

module.exports = matchPathWithDictionary;
8 changes: 4 additions & 4 deletions packages/handler-python/entryfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,11 @@ def importFromURI(uri, absl=False):
app = Flask(__name__)


@app.route(sys.argv[1] + '/', defaults={'path': ''}, methods = ['GET', 'POST'])
@app.route(sys.argv[1] +'/<path:path>', methods = ['GET', 'POST'])
def root(path):
# @app.route(sys.argv[1] + '/', defaults={'path': ''}, methods = ['GET', 'POST'])
@app.route(sys.argv[1] +'/', methods = ['GET', 'POST'])
def root(*args, **kwargs):
module = importFromURI(sys.argv[2], True)
return module.handler()
return module.handler(*args, **kwargs)


# fetch a new free port
Expand Down
8 changes: 8 additions & 0 deletions packages/handler-python/handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ module.exports = async (
isModule
) => {
return new Promise((resolve, reject) => {
// change basePath $params format to flask's format: <param>
basePath = basePath
.split("/")
.map(p => {
if (p.startsWith("$")) return "<" + p.slice(1) + ">";
return p;
})
.join("/");
var child = spawn(
pythonExe,
[path.join(__dirname, "entryfile.py"), basePath, entryFile],
Expand Down
21 changes: 9 additions & 12 deletions packages/process/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,18 +108,15 @@ function startServer(entryFile, lambdaType, handler, isModule) {

app.use(require("body-parser").urlencoded({ extended: true }));
app.use(require("body-parser").json());

app.all("*" /*[BASEPATH, path.join(BASEPATH, "/*")]*/, (req, res) => {
// if path has params (like /user/:id/:comment). Split the params into an array.
// also remove empty params (caused by path ending with slash)
if (req.params && req.params[0]) {
req.params = req.params[0]
.replace(BASEPATH.slice(1), "")
.split("/")
.filter(param => !!param);
} else {
delete req.params;
}
// change $path into express-style :path/
const pathPattern = BASEPATH.split("/")
.map(p => {
if (p.startsWith("$")) return ":" + p.slice(1);
return p;
})
.join("/");

app.all(pathPattern, (req, res) => {
try {
var globals = Object.assign(
{
Expand Down
50 changes: 50 additions & 0 deletions test/integration/dynamicPaths.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
const { get } = require("../request");
// const cheerio = require("cheerio");

test("Dynamic in root", () => {
return get("/rootdynamicpath", { json: true }).then(data => {
expect(data.rootParam).toBe("rootdynamicpath");
});
});

test("Non-dynamic index sibling", () => {
return get("/dynamicpaths", { json: true }).then(data => {
expect(data.index).toBe(true);
});
});

test("Dynamic path with param", () => {
return get("/dynamicpaths/param1", { json: true }).then(data => {
expect(data.param).toBe("param1");
});
});

test("Dynamic path's static sibling", () => {
return get("/dynamicpaths/static", { json: true }).then(data => {
expect(data.static).toBe(true);
});
});

test("Dynamic path with param and nested index", () => {
return get("/dynamicpaths/param1/nested", { json: true }).then(data => {
expect(data.nestedIndex).toBe(true);
});
});

test("Dynamic path with param and nested param", () => {
return get("/dynamicpaths/param1/nested/nestedParam1", { json: true }).then(
data => {
expect(
data.param === "param1" && data.nestedParam === "nestedParam1"
).toBe(true);
}
);
});

test("Dynamic path with param and nested static as a sibling to nested param", () => {
return get("/dynamicpaths/param1/nested/anotherStatic", { json: true }).then(
data => {
expect(data.anotherStatic).toBe(true);
}
);
});
52 changes: 32 additions & 20 deletions test/integration/matchPaths.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,24 @@ test("Txt file in root folder", () => {

test("Hidden files are hidden", () => {
// expect.assertions(1);
return get("/_hidden/config.txt").then(data => {
expect(data && data.indexOf("secret") === -1).toBe(true);
});
return get("/_hidden/config.txt")
.then(data => {
//expect(data && data.indexOf("secret") === -1).toBe(true);
})
.catch(e => {
expect(e.statusCode).toBe(404);
});
});

test("Ignore files are ignored", () => {
// expect.assertions(1);
return get("/ignoredFolder", { json: true }).then(data => {
expect(!data || (data && !data.err)).toBe(true);
});
return get("/ignoredFolder", { json: true })
.then(data => {
//expect(!data || (data && !data.err)).toBe(true);
})
.catch(e => {
expect(e.statusCode).toBe(404);
});
});

test("Static file sibling to index", () => {
Expand All @@ -51,19 +59,23 @@ test("Static file sibling to non-index files", () => {

test("path which is similar to a valid lambda but not really", () => {
// expect.assertions(1);
return get("/headings").then(data => {
expect(data).toBe("Hello");
});
return get("/headings")
.then(data => {
//expect(data).toBe("Hello");
})
.catch(e => {
expect(e.statusCode).toBe(404);
});
});

test("child path to a valid lambda", () => {
// expect.assertions(1);
return get("/react/stateless/doesntexist").then(data => {
const $ = cheerio.load(data);
expect(
$("body")
.text()
.trim()
).toBe("react-stateless");
});
});
// test("child path to a valid lambda", () => {
// // expect.assertions(1);
// return get("/react/stateless/doesntexist").then(data => {
// const $ = cheerio.load(data);
// expect(
// $("body")
// .text()
// .trim()
// ).toBe("react-stateless");
// });
// });
3 changes: 3 additions & 0 deletions test/www/$rootParam.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = (req, res)=>{
res.send({rootParam: req.params.rootParam})
}
3 changes: 3 additions & 0 deletions test/www/dynamicpaths/$param/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = (req, res)=>{
res.send({param: req.params.param})
}
3 changes: 3 additions & 0 deletions test/www/dynamicpaths/$param/nested/$nestedParam.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = (req, res)=>{
res.send({param: req.params.param, nestedParam: req.params.nestedParam})
}
3 changes: 3 additions & 0 deletions test/www/dynamicpaths/$param/nested/anotherStatic.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = (req, res)=>{
res.send({anotherStatic: true})
}
3 changes: 3 additions & 0 deletions test/www/dynamicpaths/$param/nested/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = (req, res)=>{
res.send({nestedIndex: true})
}
3 changes: 3 additions & 0 deletions test/www/dynamicpaths/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = (req, res)=>{
res.send({index: true})
}
5 changes: 5 additions & 0 deletions test/www/dynamicpaths/nonparam/$nestedParam.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from flask import jsonify
def handler(nestedParam):
return jsonify(
nestedParam=nestedParam
)
Loading

0 comments on commit 74ff544

Please sign in to comment.