phaedra 0.5.2 → 0.5.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +107 -17
- data/example/Dockerfile +1 -1
- data/example/Gemfile +1 -1
- data/example/README.md +19 -0
- data/example/api/env.rb +15 -0
- data/example/api/{the-time.rb → params.rb} +3 -3
- data/example/phaedra/initializers.rb +8 -5
- data/lib/phaedra/rack_app.rb +8 -1
- data/lib/phaedra/version.rb +1 -1
- metadata +5 -4
- data/example/api/simple.rb +0 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 65ebfc3c9c8023c4a680704a085d9b4b62505fce1df1a82ca6d57bd9785c5ec2
|
4
|
+
data.tar.gz: d3384f2fa5d94ba594427970aad49cabe47b1d59d6f2a3289ba608c413e8d3db
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c40e0c8017d3bb172a144b365ecaf62bc1d9b602c575740fefff8e0876ab54d853d1c5434aa2da8b8f02cda11d8f43084b5354d86464195374ee76799fd2e9c5
|
7
|
+
data.tar.gz: 763f60081477658fd4cfad3461cc8f9148c3c1c3e4d7f20d4571e1cf4d136c24c55a6aecfbc2c39c4ec3471bfadf7cca101be51dc7f3bcceb72c5e4eb21a2cdd
|
data/README.md
CHANGED
@@ -1,9 +1,15 @@
|
|
1
1
|
# Phaedra: Serverless Ruby Functions
|
2
2
|
|
3
|
-
Phaedra is a web microframework for writing serverless Ruby functions. They are isolated pieces of logic which respond to HTTP requests (GET, POST, etc.) and typically get mounted at a particular URL path. They can be tested locally and deployed to a supported serverless hosting platform or to any [Rack-compatible web server](https://github.com/rack/rack).
|
3
|
+
Phaedra is a web microframework for writing serverless Ruby functions. They are isolated pieces of logic which respond to HTTP requests (GET, POST, etc.) and typically get mounted at a particular URL path. They can be tested locally and deployed to a supported serverless hosting platform, using a container via Docker & Docker Compose, or to any [Rack-compatible web server](https://github.com/rack/rack).
|
4
|
+
|
5
|
+
Phaedra is well-suited for building an API layer which you attach to a static site (aka [the Jamstack](https://www.bridgetownrb.com/docs/jamstack)) to provide dynamic functionality accessible any time after the static site loads in the browser.
|
4
6
|
|
5
7
|
Serverless compatibility is presently focused on [Vercel](https://vercel.com) and [OpenFaaS](https://openfaas.com), but there are likely additional platforms we'll be adding support for in the future.
|
6
8
|
|
9
|
+
For swift deployment via Docker, we recommend [Fly.io](https://fly.io).
|
10
|
+
|
11
|
+
(P.S. Wondering how you can deploy a static site on [Netlify](https://www.netlify.com) and still use a Ruby API? Scroll down for a suggested approach!)
|
12
|
+
|
7
13
|
## Installation
|
8
14
|
|
9
15
|
Add this line to your application's Gemfile:
|
@@ -24,18 +30,26 @@ Or install it yourself as:
|
|
24
30
|
$ gem install phaedra
|
25
31
|
```
|
26
32
|
|
33
|
+
## Examples
|
34
|
+
|
35
|
+
[Here's an example](https://github.com/whitefusionhq/phaedra/tree/master/example) of what the structure of a typical Phaedra app looks like. It includes `config.ru` for booting it up as a Rack app using Puma, as well as a `Dockerfile` and `docker-compose.yml` so you can run the app containerized in virtually any development or production hosting environment.
|
36
|
+
|
37
|
+
[Here's a demo](https://phaedra-demo.whitefusion.design/api/env) of one of the functions. [And another one.](https://phaedra-demo.whitefusion.design/api/params?search=Waiting%20for%20Guffman)
|
38
|
+
|
27
39
|
## Usage
|
28
40
|
|
29
41
|
Functions are single Ruby files which respond to a URL path (aka `/api/path/to/function`). The path is determined by the location of the file on the filesystem relative to the functions root (aka `api`). So, given a path of `./api/folder/run-me.rb`, the URL path would be `/api/folder/run-me`.
|
30
42
|
|
31
43
|
Functions are written as subclasses of `Phaedra::Base` using the name `PhaedraFunction`. The `params` argument is a Hash containing the parsed contents of the incoming query string, form data, or body JSON. The response object returned by your function is typically a Hash which will be transformed into JSON output automatically, but it can also be plain text.
|
32
44
|
|
45
|
+
Code to be run once upon function initialization and shared between multiple functions should be placed in the `phaedra/initializers.rb` file (see more on that below).
|
46
|
+
|
33
47
|
Some platforms such as Vercel require the function class name to be `Handler`, so you can put that at the bottom of your file for full compatibility.
|
34
48
|
|
35
49
|
Here's a basic example:
|
36
50
|
|
37
51
|
```ruby
|
38
|
-
|
52
|
+
require_relative "../phaedra/initializers"
|
39
53
|
|
40
54
|
class PhaedraFunction < Phaedra::Base
|
41
55
|
def get(params)
|
@@ -60,17 +74,23 @@ Functions can define `action` callbacks:
|
|
60
74
|
```ruby
|
61
75
|
class PhaedraFunction < Phaedra::Base
|
62
76
|
before_action :do_stuff_before
|
63
|
-
after_action
|
77
|
+
after_action do
|
78
|
+
# process response object further...
|
79
|
+
end
|
64
80
|
around_action :do_it_all_around
|
65
81
|
|
66
82
|
def do_stuff_before
|
67
|
-
#
|
83
|
+
# process request object before action handler...
|
68
84
|
end
|
69
85
|
|
70
|
-
|
86
|
+
def do_it_all_around
|
87
|
+
# run code before
|
88
|
+
yield
|
89
|
+
# run code after
|
90
|
+
end
|
71
91
|
|
72
92
|
def get(params)
|
73
|
-
# this will be run within the callback chain
|
93
|
+
# this will be run within the entire callback chain
|
74
94
|
end
|
75
95
|
end
|
76
96
|
```
|
@@ -79,34 +99,51 @@ You can modify the `request` object in a `before_action` callback to perform set
|
|
79
99
|
|
80
100
|
### Shared Code You Only Want to Run Once
|
81
101
|
|
82
|
-
|
102
|
+
Phaedra provides a default location to place shared modules and code that should be run once upon first deployment of your functions. This is particularly useful when setting up a database connection or performing expensive operations you only want to do once, rather than for every request.
|
103
|
+
|
104
|
+
Here's an example of how that works:
|
83
105
|
|
84
106
|
```ruby
|
85
107
|
# api/run-it-once.rb
|
86
108
|
|
87
|
-
|
88
|
-
require_relative "../lib/shared_code"
|
109
|
+
require_relative "../phaedra/initializers"
|
89
110
|
|
90
111
|
class PhaedraFunction < Phaedra::Base
|
91
112
|
def get(params)
|
92
|
-
"Run it once! #{
|
113
|
+
"Run it once! #{Phaedra::Shared.run_once} / #{Time.now}"
|
93
114
|
end
|
94
115
|
end
|
95
116
|
```
|
96
117
|
|
97
118
|
```ruby
|
98
|
-
#
|
119
|
+
# phaedra/initializers.rb
|
120
|
+
|
121
|
+
module Phaedra
|
122
|
+
module Shared
|
123
|
+
Initializers.register self do
|
124
|
+
run_once
|
125
|
+
end
|
99
126
|
|
100
|
-
|
101
|
-
|
102
|
-
|
127
|
+
def self.run_once
|
128
|
+
@only_once ||= Time.now
|
129
|
+
end
|
103
130
|
end
|
104
131
|
end
|
105
132
|
```
|
106
133
|
|
107
|
-
Now each time you invoke the function at `/api/run-it-once`, the
|
134
|
+
Now each time you invoke the function at `/api/run-it-once`, the timestamp will never change until the next redeployment.
|
135
|
+
|
136
|
+
**NOTE:** When running in a Rack-based configuration (see below), Ruby's `load` method is invoked for every request to any Phaedra function. This means Ruby has to parse and compile the code in your function each time. For small functions this happens extremely quickly, but if you find yourself writing a large function and seeing some performance slowdowns, consider extracting most of the function code to additional Ruby files and using the `require_relative` technique as mentioned above. The Ruby code in those required files will only be compiled once and all classes/modules/etc. will be saved in memory until the next redeployment.
|
108
137
|
|
109
|
-
|
138
|
+
## Environment
|
139
|
+
|
140
|
+
You can set the environment of your Phaedra app using the `PHAEDRA_ENV` environment variable. That is then available via the `Phaedra.environment` method. By default, the value is `:development`.
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
# ENV["PHAEDRA_ENV"] == "production"
|
144
|
+
|
145
|
+
Phaedra.environment == :production # true
|
146
|
+
```
|
110
147
|
|
111
148
|
## Deployment
|
112
149
|
|
@@ -252,6 +289,45 @@ server {
|
|
252
289
|
|
253
290
|
Change the `server_name`, `root`, and `passenger_ruby` paths to your particular setup and you'll be good to go. (If you run into any errors, double-check there's a `config.ru` in the parent folder of your site destination folder.)
|
254
291
|
|
292
|
+
### Docker
|
293
|
+
|
294
|
+
[In the example app provided](https://github.com/whitefusionhq/phaedra/tree/master/example), there is a `config.ru` file for booting it up as a Rack app using Puma. The `Dockerfile` and `docker-compose.yml` files allow you to easily build and deploy the app at port 8080 (but that can easily be changed). Using the Docker Compose commands:
|
295
|
+
|
296
|
+
```sh
|
297
|
+
# Build (if necessary) and deploy:
|
298
|
+
docker-compose up
|
299
|
+
|
300
|
+
# Get information on the running container:
|
301
|
+
docker-compose ps
|
302
|
+
|
303
|
+
# Inspect the output logs:
|
304
|
+
docker-compose logs
|
305
|
+
|
306
|
+
# Exit the running container:
|
307
|
+
docker-compose down
|
308
|
+
|
309
|
+
# If you make changes to the code and need to rebuild:
|
310
|
+
docker-compose up --build
|
311
|
+
```
|
312
|
+
|
313
|
+
#### Fly.io
|
314
|
+
|
315
|
+
Deploying your Phaedra app's Docker container via [Fly.io](https://fly.io) couldn't be easier. Simply create a new app and deploy using Fly.io's command line utility:
|
316
|
+
|
317
|
+
```sh
|
318
|
+
# Create the new app using your Fly.io account:
|
319
|
+
flyctl apps create
|
320
|
+
|
321
|
+
# Deploy using the Dockerfile:
|
322
|
+
flyctl deploy
|
323
|
+
|
324
|
+
# Print out the URL and other info on your new app:
|
325
|
+
flyctl info
|
326
|
+
|
327
|
+
# Change the Phaedra app environment:
|
328
|
+
flyctl secrets set PHAEDRA_ENV=production
|
329
|
+
```
|
330
|
+
|
255
331
|
### WEBrick
|
256
332
|
|
257
333
|
Integrating Phaedra into a WEBrick server is pretty straightforward. Given a `server` object, it can be accomplished thusly:
|
@@ -286,7 +362,21 @@ load File.join(Dir.pwd, "api", "func.rb")
|
|
286
362
|
@server.mount '/path', Handler
|
287
363
|
```
|
288
364
|
|
289
|
-
This method precludes any automatic routing by Phaedra, so it's discouraged unless you are using WEBrick within a larger setup that utilizes its own routing method. (Interestingly enough, that's how Vercel works under the hood.)
|
365
|
+
This method precludes any automatic routing by Phaedra, so it's discouraged unless you are using WEBrick within a larger setup that utilizes its own routing method. (Interestingly enough, [that's how Vercel works under the hood](https://github.com/vercel/vercel/blob/master/packages/now-ruby/now_init.rb).)
|
366
|
+
|
367
|
+
## Connecting a Static Site on Netlify to a Phaedra API
|
368
|
+
|
369
|
+
[Netlify](https://www.netlify.com) is a popular hosting solution for Jamstack (static) sites, but its serverless functions feature doesn't support Ruby. However, using proxy rewrites, you can deploy the static site part of your repository to Netlify and set the `/api` endpoint to route requests to your Phaedra app on the fly (hosted elsewhere).
|
370
|
+
|
371
|
+
For example, if your Phaedra app is hosted on Fly.io (see above), you'll want Netlify's CDN to proxy all requests to `/api/*` to that app's URL. We can accomplish that by adding a `_redirects` file to the static site's source folder (for Bridgetown sites, that's `src`):
|
372
|
+
|
373
|
+
```
|
374
|
+
/api/* https://super-awesome-phaedra-api.fly.dev/api/:splat 200
|
375
|
+
```
|
376
|
+
|
377
|
+
Once that deploys, you can go to your Netlify site URL, append `/api/whatever`, and under-the-hood that will connect to `https://super-awesome-phaedra-api.fly.dev/api/whatever` in a completely transparent manner.
|
378
|
+
|
379
|
+
If you want to change the proxy URL for different contexts (staging vs. production, etc.), you can follow Netlify's "[Separate _redirects files for separate contexts or branches](https://community.netlify.com/t/support-guide-making-redirects-work-for-you-troubleshooting-and-debugging/13433)" instructions here.
|
290
380
|
|
291
381
|
----
|
292
382
|
|
data/example/Dockerfile
CHANGED
@@ -17,7 +17,7 @@ ARG DOCKER_USER=${DOCKER_USER:-user}
|
|
17
17
|
ARG APP_DIR=${APP_DIR:-/home/user/phaedra-app}
|
18
18
|
|
19
19
|
# Change with --build-arg PHAEDRA_ENV=production
|
20
|
-
ARG PHAEDRA_ENV=
|
20
|
+
ARG PHAEDRA_ENV=development
|
21
21
|
ENV PHAEDRA_ENV=$PHAEDRA_ENV
|
22
22
|
|
23
23
|
# Create a non-root user
|
data/example/Gemfile
CHANGED
data/example/README.md
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# Example Phaedra App
|
2
|
+
|
3
|
+
This will spin up a Phaedra demo API running on port 8080 (which can be changed).
|
4
|
+
|
5
|
+
## Local Exec
|
6
|
+
|
7
|
+
Requires Ruby 2.6 or greater.
|
8
|
+
|
9
|
+
```sh
|
10
|
+
bundle install
|
11
|
+
|
12
|
+
bundle exec rackup -p 8080
|
13
|
+
```
|
14
|
+
|
15
|
+
## Docker Compose
|
16
|
+
|
17
|
+
```sh
|
18
|
+
docker-compose up
|
19
|
+
```
|
data/example/api/env.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative "../phaedra/initializers"
|
2
|
+
|
3
|
+
class PhaedraFunction < Phaedra::Base
|
4
|
+
def get(params)
|
5
|
+
response["Content-Type"] = "text/html; charset=utf-8"
|
6
|
+
<<~HTML
|
7
|
+
<p>Hello friend! 😃</p>
|
8
|
+
<p>Startup Time: #{Phaedra::Shared.the_time}</p>
|
9
|
+
<p>Environment: #{Phaedra.environment}</p>
|
10
|
+
<p>Current Time: #{Time.new}</p>
|
11
|
+
HTML
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
Handler = PhaedraFunction
|
@@ -4,7 +4,7 @@ class PhaedraFunction < Phaedra::Base
|
|
4
4
|
before_action :earlier_stuff
|
5
5
|
|
6
6
|
def get(params)
|
7
|
-
"The ?search param is #{params[:search]}"
|
7
|
+
"The ?search= param is #{params[:search] || "-missing-"}"
|
8
8
|
end
|
9
9
|
|
10
10
|
def post(params)
|
@@ -14,10 +14,10 @@ class PhaedraFunction < Phaedra::Base
|
|
14
14
|
private
|
15
15
|
|
16
16
|
def earlier_stuff
|
17
|
-
request.query["search"] += "
|
17
|
+
request.query["search"] += " (nice!)" if request.query["search"]
|
18
18
|
|
19
19
|
if request.body
|
20
|
-
request.body.sub!("Works", "
|
20
|
+
request.body.sub!("Works", "Totally Works")
|
21
21
|
end
|
22
22
|
end
|
23
23
|
end
|
@@ -1,11 +1,14 @@
|
|
1
1
|
require "phaedra"
|
2
|
+
require "securerandom"
|
2
3
|
|
3
4
|
module Phaedra
|
4
|
-
|
5
|
-
|
6
|
-
|
5
|
+
module Shared
|
6
|
+
Initializers.register self do
|
7
|
+
the_time SecureRandom.hex(10)
|
8
|
+
end
|
7
9
|
|
8
|
-
|
9
|
-
|
10
|
+
def self.the_time(init = nil)
|
11
|
+
@the_time ||= "#{Time.now} (random seed: #{init})"
|
12
|
+
end
|
10
13
|
end
|
11
14
|
end
|
data/lib/phaedra/rack_app.rb
CHANGED
@@ -8,11 +8,18 @@ module Phaedra
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def header
|
11
|
-
@env.dup.transform_keys do |key|
|
11
|
+
@transformed_headers ||= @env.dup.transform_keys do |key|
|
12
12
|
key.respond_to?(:downcase) ? key.downcase : key
|
13
|
+
end.tap do |headers|
|
14
|
+
# TODO: normalize a few common headers
|
15
|
+
headers["authorization"] = headers["http_authorization"]
|
13
16
|
end
|
14
17
|
end
|
15
18
|
|
19
|
+
def [](key)
|
20
|
+
header[key]
|
21
|
+
end
|
22
|
+
|
16
23
|
def body
|
17
24
|
@request_body ||= "" + get_header(Rack::RACK_INPUT).read
|
18
25
|
end
|
data/lib/phaedra/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: phaedra
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.5.
|
4
|
+
version: 0.5.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jared White
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-06-
|
11
|
+
date: 2020-06-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -82,8 +82,9 @@ files:
|
|
82
82
|
- Rakefile
|
83
83
|
- example/Dockerfile
|
84
84
|
- example/Gemfile
|
85
|
-
- example/
|
86
|
-
- example/api/
|
85
|
+
- example/README.md
|
86
|
+
- example/api/env.rb
|
87
|
+
- example/api/params.rb
|
87
88
|
- example/config.ru
|
88
89
|
- example/docker-compose.yml
|
89
90
|
- example/phaedra/initializers.rb
|
data/example/api/simple.rb
DELETED
@@ -1,10 +0,0 @@
|
|
1
|
-
require_relative "../phaedra/initializers"
|
2
|
-
|
3
|
-
class PhaedraFunction < Phaedra::Base
|
4
|
-
def get(params)
|
5
|
-
response["Content-Type"] = "text/html; charset=utf-8"
|
6
|
-
"<p>😁 #{Phaedra.the_time} - #{ENV["PHAEDRA_ENV"]} - #{Time.new}</p>"
|
7
|
-
end
|
8
|
-
end
|
9
|
-
|
10
|
-
Handler = PhaedraFunction
|