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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 00d5ba6f3bc10860baef3bf63867665db4ef610b7cf14a8281495cf929339bc3
4
- data.tar.gz: 5d1c500a8cbe79a89182497e7881fdeba3d129e5e2f2ddcce19012f9ae713171
3
+ metadata.gz: 65ebfc3c9c8023c4a680704a085d9b4b62505fce1df1a82ca6d57bd9785c5ec2
4
+ data.tar.gz: d3384f2fa5d94ba594427970aad49cabe47b1d59d6f2a3289ba608c413e8d3db
5
5
  SHA512:
6
- metadata.gz: 3f3a8145df4a02f5362dfffb66f1de44ccd650425504007f2f49fa666c68bd9d228d0e8ade98daa2d318c57b220013009bd2c9b37d10d375a7cecca2cee0c4ac
7
- data.tar.gz: f0effbf926248e56c67da411bd688cc3f6783d7710bdedf141029d523b4e55667df18b1e509cb68aa55795a01b74369ac8e007a91754ac9b3c6046741d166258
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
- require "phaedra"
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 :do_stuff_after
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
- # code
83
+ # process request object before action handler...
68
84
  end
69
85
 
70
- # do_stuff_after, etc.
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
- You can use `require_relative` to load and execute shared Ruby code from another folder, say `lib`. 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.
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
- require "phaedra"
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! #{SharedCode.run_once} / #{Time.now}"
113
+ "Run it once! #{Phaedra::Shared.run_once} / #{Time.now}"
93
114
  end
94
115
  end
95
116
  ```
96
117
 
97
118
  ```ruby
98
- # lib/shared_code.rb
119
+ # phaedra/initializers.rb
120
+
121
+ module Phaedra
122
+ module Shared
123
+ Initializers.register self do
124
+ run_once
125
+ end
99
126
 
100
- module SharedCode
101
- def self.run_once
102
- @one_time ||= Time.now
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 first timestamp will never change until the next redeployment.
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
- **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 use 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.
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
 
@@ -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=staging
20
+ ARG PHAEDRA_ENV=development
21
21
  ENV PHAEDRA_ENV=$PHAEDRA_ENV
22
22
 
23
23
  # Create a non-root user
@@ -1,4 +1,4 @@
1
1
  source "https://rubygems.org"
2
2
 
3
- gem "phaedra", path: ".."
3
+ gem "phaedra"
4
4
  gem "puma"
@@ -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
+ ```
@@ -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"] += " SEARCH!" if request.query["search"]
17
+ request.query["search"] += " (nice!)" if request.query["search"]
18
18
 
19
19
  if request.body
20
- request.body.sub!("Works", "Lurks")
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
- Initializers.register self do
5
- the_time("123")
6
- end
5
+ module Shared
6
+ Initializers.register self do
7
+ the_time SecureRandom.hex(10)
8
+ end
7
9
 
8
- def self.the_time(init = nil)
9
- @the_time ||= "#{Time.now} + #{init}"
10
+ def self.the_time(init = nil)
11
+ @the_time ||= "#{Time.now} (random seed: #{init})"
12
+ end
10
13
  end
11
14
  end
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Phaedra
2
- VERSION = "0.5.2"
2
+ VERSION = "0.5.3"
3
3
  end
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.2
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-24 00:00:00.000000000 Z
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/api/simple.rb
86
- - example/api/the-time.rb
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
@@ -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