phaedra 0.2.0 → 0.4.1

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: 50fe608ca2567e6f1262fcfd1d7a9cc4171a5b467989b115692442dd1ae82e4a
4
- data.tar.gz: 3a0da1de124ebd66b6d70ab25a9eee6e9288f28bad0adca7b800e8d82ecd0c31
3
+ metadata.gz: 47c1b79ae715e83f3dd098fb9d0d29c5d89299f96c01d084f1cb44573f216022
4
+ data.tar.gz: e716ca8ef384fdeadd773485042099d7bf7840b007e636588ce6fb28e40ab4b3
5
5
  SHA512:
6
- metadata.gz: 6ef12e516dbbbfdbc487554d6422bfba931933598934a960c71bb4946b31c3d0b9e2e568bd818b1dd3c2bfbda5ebd556af99f8df62284f33d14ff8578b30c1b9
7
- data.tar.gz: ae9bd0c2850334dd67b58e18e19a3444b7230539c45fb1b995b7441240930abecb3ff47c6211da5cb2bd2998b4454f3a7a6390fe7f2509d2718e2af13b0932f5
6
+ metadata.gz: cf41193d15f6bc3678ebccd9239e7185479b3bd4029f593887202ca3e88b02529702688f64f2b373c90e54fed766b264a9f12c2a8a39e4c90039d0d708d9b89a
7
+ data.tar.gz: 23686c007d52d61763ac393ced71d6485dbf3b3c45cc153828950f6e9c1e8018934834a0dfb90a7908d3357334c3542b34e74629d9774663607f86b21080fb94
data/README.md CHANGED
@@ -1,10 +1,8 @@
1
1
  # Phaedra: Serverless Ruby Functions
2
2
 
3
- _NOTE: not yet released! Check back in June 2020!_
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).
4
4
 
5
- Phaedra is a REST 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).
6
-
7
- Serverless compatibility is presently focused on [Vercel](https://vercel.com), but there are likely additional platforms we'll be adding support for in the future (OpenFaaS, Fission, etc.).
5
+ 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.
8
6
 
9
7
  ## Installation
10
8
 
@@ -32,7 +30,7 @@ Functions are single Ruby files which respond to a URL path (aka `/api/path/to/f
32
30
 
33
31
  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.
34
32
 
35
- Some platforms require the function class name to be `Handler`, so you can put that at the bottom of your file for full compatibility.
33
+ 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.
36
34
 
37
35
  Here's a basic example:
38
36
 
@@ -55,7 +53,7 @@ Your function can support `get`, `post`, `put`, `patch`, and `delete` methods wh
55
53
 
56
54
  Each method is provided access to `request` and `response` objects. If your function was directly instantiated by WEBrick, those will be `WEBrick::HTTPRequest` and `WEBrick::HTTPResponse` respectively. If your function was instantiated by Rack, those will be `Phaedra::Request` (a thin wrapper around `Rack::Request`) and `Rack::Response` respectively.
57
55
 
58
- ## Callbacks
56
+ ### Callbacks
59
57
 
60
58
  Functions can define `action` callbacks:
61
59
 
@@ -79,9 +77,137 @@ end
79
77
 
80
78
  You can modify the `request` object in a `before_action` callback to perform setup tasks before the actions are executed, or you can modify `response` in a `after_action` to further process the response.
81
79
 
82
- ## Rack
80
+ ### Shared Code You Only Want to Run Once
81
+
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.
83
+
84
+ ```ruby
85
+ # api/run-it-once.rb
86
+
87
+ require "phaedra"
88
+ require_relative "../lib/shared_code"
89
+
90
+ class PhaedraFunction < Phaedra::Base
91
+ def get(params)
92
+ "Run it once! #{SharedCode.run_once} / #{Time.now}"
93
+ end
94
+ end
95
+ ```
96
+
97
+ ```ruby
98
+ # lib/shared_code.rb
99
+
100
+ module SharedCode
101
+ def self.run_once
102
+ @one_time ||= Time.now
103
+ end
104
+ end
105
+ ```
106
+
107
+ Now each time you invoke the function at `/api/run-it-once`, the first timestamp will never change until the next redeployment.
108
+
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.
110
+
111
+ ## Deployment
112
+
113
+ ### Vercel
114
+
115
+ All you have to do is create a static site repo ([Bridgetown](https://www.bridgetownrb.com), Jekyll, Middleman, etc.) with an `api` folder and Vercel will automatically set up the serverless functions every time there's a new branch or production deployment. As mentioned above, you'll need to ensure you add `Handler = PhaedraFunction` to the bottom of each Ruby function.
116
+
117
+ ### OpenFaaS
118
+
119
+ We recommend using OpenFaaS' dockerfile template so you can define your own `Dockerfile` to book Rack + Phaedra using the Puma web server. This also allows you to customize the Docker image configuration to install and configure other tools as necessary.
120
+
121
+ First, make sure you've pulled down the template:
122
+
123
+ ```sh
124
+ faas-cli template store pull dockerfile
125
+ ```
126
+
127
+ Then add a `Dockerfile` to your OpenFaaS project's function folder (e.g., `testphaedra`):
128
+
129
+ ```dockerfile
130
+ # testphaedra/Dockerfile
131
+
132
+ FROM openfaas/of-watchdog:0.7.7 as watchdog
133
+
134
+ FROM ruby:2.6.6-slim-stretch
135
+
136
+ COPY --from=watchdog /fwatchdog /usr/bin/fwatchdog
137
+ RUN chmod +x /usr/bin/fwatchdog
138
+
139
+ ARG ADDITIONAL_PACKAGE
140
+ RUN apt-get update \
141
+ && apt-get install -qy --no-install-recommends build-essential ${ADDITIONAL_PACKAGE}
142
+
143
+ WORKDIR /home/app
144
+
145
+ # Use cache layer for Gemfile
146
+ COPY Gemfile .
147
+ RUN bundle install
148
+ RUN gem install puma -N
149
+
150
+ # Copy over the rest
151
+ COPY . .
83
152
 
84
- Booting Phaedra up as Rack app is very simple. All you need is a `config.ru` file alongside your `api` folder:
153
+ # Create a non-root user
154
+ RUN addgroup --system app \
155
+ && adduser --system --ingroup app app
156
+ RUN chown app:app -R /home/app
157
+ USER app
158
+
159
+ # Run Puma as the server process
160
+ ENV fprocess="puma -p 5000"
161
+
162
+ EXPOSE 8080
163
+
164
+ HEALTHCHECK --interval=2s CMD [ -e /tmp/.lock ] || exit 1
165
+
166
+ ENV upstream_url="http://127.0.0.1:5000"
167
+ ENV mode="http"
168
+
169
+ CMD ["fwatchdog"]
170
+ ```
171
+
172
+ Next add the `config.ru` file to boot Rack:
173
+
174
+ ```ruby
175
+ # testphaedra/config.ru
176
+
177
+ require "phaedra"
178
+
179
+ run Phaedra::RackApp.new
180
+ ```
181
+
182
+ Finally, add a YAML file that lives alongside your function folder:
183
+
184
+ ```yaml
185
+ # testphaedra.yml
186
+
187
+ version: 1.0
188
+ provider:
189
+ name: openfaas
190
+ gateway: http://127.0.0.1:8080
191
+ functions:
192
+ testphaedra:
193
+ lang: dockerfile
194
+ handler: ./testphaedra
195
+ image: yourdockerusername/testphaedra:latest
196
+ ```
197
+
198
+ (Replace `yourdockerusername` with your [Docker Hub](https://hub.docker.com) username.)
199
+
200
+ Now run `faas-cli up -f testphaedra.yml` to build and deploy the function. Given the Ruby function `testphaedra/api/run-me.rb`, you'd call it like so:
201
+
202
+ ```sh
203
+ curl http://127.0.0.1:8080/function/testphaedra/api/run-me
204
+ ```
205
+
206
+ In case you're wondering: yes, with Phaedra you can write multiple Ruby functions which will be accessible via different URL paths—all handled by a single OpenFaaS function. Of course it's possible set up multiple Phaedra projects and deploy them as separate OpenFaaS functions if you wish.
207
+
208
+ ### Rack
209
+
210
+ Booting Phaedra up as Rack app is very simple. All you need to do is add a `config.ru` file alongside your `api` folder:
85
211
 
86
212
  ```ruby
87
213
  require "phaedra"
@@ -89,9 +215,38 @@ require "phaedra"
89
215
  run Phaedra::RackApp.new
90
216
  ```
91
217
 
92
- Then run `rackup` in the terminal.
218
+ Then run `rackup` in the terminal, or use another Rack-compatible server like Puma or Passenger.
219
+
220
+ The settings (and their defaults) you can pass to the `new` method are as follows:
93
221
 
94
- ## WEBrick
222
+ ```ruby
223
+ {
224
+ "root_dir" => Dir.pwd,
225
+ "serverless_api_dir" => "api"
226
+ }
227
+ ```
228
+
229
+ Wondering if you can deploy a static site with an `api` folder via Nginx + Passenger? Yes, you can! Just configure your `my_site.conf` file like so:
230
+
231
+ ```nginxconf
232
+ server {
233
+ listen 80;
234
+ server_name www.domain.com;
235
+
236
+ # Tell Nginx and Passenger where your site destination folder is
237
+ root /home/me/my_site/output;
238
+
239
+ # Turn on Passenger
240
+ location /api {
241
+ passenger_enabled on;
242
+ passenger_ruby /usr/local/rvm/gems/ruby-2.6.6@mysite/wrappers/ruby;
243
+ }
244
+ }
245
+ ```
246
+
247
+ 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.)
248
+
249
+ ### WEBrick
95
250
 
96
251
  Integrating Phaedra into a WEBrick server is pretty straightforward. Given a `server` object, it can be accomplished thusly:
97
252
 
@@ -105,13 +260,13 @@ server.mount_proc "/#{base_api_folder}" do |req, res|
105
260
  ruby_path = File.join(full_api_path, api_folder, "#{endpoint}.rb")
106
261
 
107
262
  if File.exist?(ruby_path)
108
- original_verbosity = $VERBOSE
109
- $VERBOSE = nil
263
+ if Object.constants.include?(:PhaedraFunction)
264
+ Object.send(:remove_const, :PhaedraFunction)
265
+ end
110
266
  load ruby_path
111
- $VERBOSE = original_verbosity
112
267
 
113
- handler = Handler.new(server)
114
- handler.service(req, res)
268
+ func = PhaedraFunction.new
269
+ func.service(req, res)
115
270
  else
116
271
  raise HTTPStatus::NotFound, "`#{req.path}' not found."
117
272
  end
@@ -125,6 +280,8 @@ load File.join(Dir.pwd, "api", "func.rb")
125
280
  @server.mount '/path', Handler
126
281
  ```
127
282
 
283
+ 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.)
284
+
128
285
  ----
129
286
 
130
287
  ## Development
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "phaedra", path: ".."
4
+ gem "puma"
@@ -0,0 +1,10 @@
1
+ require "phaedra"
2
+
3
+ class PhaedraFunction < Phaedra::Base
4
+ def get(params)
5
+ response["Content-Type"] = "text/html; charset=utf-8"
6
+ "<p>This is Interesting. 😁</p>"
7
+ end
8
+ end
9
+
10
+ Handler = PhaedraFunction
@@ -4,7 +4,6 @@ class PhaedraFunction < Phaedra::Base
4
4
  before_action :earlier_stuff
5
5
 
6
6
  def get(params)
7
- response["Content-Type"] = 'text/plain; charset=utf-8'
8
7
  "The Current Time is: #{Time.new} and Search Param is #{params[:search]}."
9
8
  end
10
9
 
@@ -112,7 +112,7 @@ module Phaedra
112
112
 
113
113
  def set_initial_status
114
114
  @res.status = 200
115
- @res["Content-Type"] = 'application/json'
115
+ @res["Content-Type"] = "application/json"
116
116
  end
117
117
 
118
118
  def call_method_action(params)
@@ -121,7 +121,11 @@ module Phaedra
121
121
  end
122
122
 
123
123
  def complete_response
124
- @res.body = @res.body.to_json if @res["Content-Type"] == "application/json"
124
+ if @res.body.is_a?(String) && !@res["Content-Type"].start_with?("text/")
125
+ @res["Content-Type"] = "text/plain; charset=utf-8"
126
+ elsif @res["Content-Type"] == "application/json"
127
+ @res.body = @res.body.to_json
128
+ end
125
129
  end
126
130
 
127
131
  def error_condition
@@ -0,0 +1,2 @@
1
+ require "phaedra/middleware/not_found"
2
+ require "phaedra/middleware/static"
@@ -0,0 +1,21 @@
1
+ module Phaedra
2
+ module Middleware
3
+ class NotFound
4
+ def initialize(app, path, content_type = 'text/html; charset=utf-8')
5
+ @app = app
6
+ @content = File.read(path)
7
+ @length = @content.bytesize.to_s
8
+ @content_type = content_type
9
+ end
10
+
11
+ def call(env)
12
+ response = @app.call(env)
13
+ if response[0] == 404
14
+ [404, {'Content-Type' => @content_type, 'Content-Length' => @length}, [@content]]
15
+ else
16
+ response
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,26 @@
1
+ module Phaedra
2
+ module Middleware
3
+ # Based on Rack::TryStatic middleware
4
+ # https://github.com/rack/rack-contrib/blob/master/lib/rack/contrib/try_static.rb
5
+
6
+ class Static
7
+ def initialize(app, options)
8
+ @app = app
9
+ @try = ["", ".html", "index.html", "/index.html", *options[:try]]
10
+ @static = Rack::Static.new(
11
+ lambda { |_| [404, {}, []] },
12
+ options)
13
+ end
14
+
15
+ def call(env)
16
+ orig_path = env['PATH_INFO']
17
+ found = nil
18
+ @try.each do |path|
19
+ resp = @static.call(env.merge!({'PATH_INFO' => orig_path + path}))
20
+ break if !(403..405).include?(resp[0]) && found = resp
21
+ end
22
+ found or @app.call(env.merge!('PATH_INFO' => orig_path))
23
+ end
24
+ end
25
+ end
26
+ end
@@ -11,7 +11,7 @@ module Phaedra
11
11
  end
12
12
 
13
13
  def body
14
- @request_body ||= get_header(Rack::RACK_INPUT).read
14
+ @request_body ||= "" + get_header(Rack::RACK_INPUT).read
15
15
  end
16
16
  end
17
17
 
@@ -33,10 +33,10 @@ module Phaedra
33
33
  endpoint = File.basename(req.path)
34
34
  ruby_path = File.join(full_api_path, api_folder, "#{endpoint}.rb")
35
35
  if File.exist?(ruby_path)
36
- original_verbosity = $VERBOSE
37
- $VERBOSE = nil
36
+ if Object.constants.include?(:PhaedraFunction)
37
+ Object.send(:remove_const, :PhaedraFunction)
38
+ end
38
39
  load ruby_path
39
- $VERBOSE = original_verbosity
40
40
 
41
41
  func = PhaedraFunction.new
42
42
  func.service(req, res)
@@ -1,3 +1,3 @@
1
1
  module Phaedra
2
- VERSION = "0.2.0"
2
+ VERSION = "0.4.1"
3
3
  end
@@ -6,7 +6,7 @@ Gem::Specification.new do |spec|
6
6
  spec.authors = ["Jared White"]
7
7
  spec.email = ["jared@whitefusion.io"]
8
8
 
9
- spec.summary = %q{Write serverless Ruby functions via a REST microframework compatible with Rack or WEBrick.}
9
+ spec.summary = %q{Write serverless Ruby functions via a REST-like microframework compatible with Rack or WEBrick.}
10
10
  spec.description = spec.summary
11
11
  spec.homepage = "https://github.com/whitefusionhq/phaedra"
12
12
  spec.license = "MIT"
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.2.0
4
+ version: 0.4.1
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-06 00:00:00.000000000 Z
11
+ date: 2020-06-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -66,7 +66,7 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '12.0'
69
- description: Write serverless Ruby functions via a REST microframework compatible
69
+ description: Write serverless Ruby functions via a REST-like microframework compatible
70
70
  with Rack or WEBrick.
71
71
  email:
72
72
  - jared@whitefusion.io
@@ -80,11 +80,16 @@ files:
80
80
  - LICENSE.txt
81
81
  - README.md
82
82
  - Rakefile
83
+ - example/Gemfile
84
+ - example/api/simple.rb
83
85
  - example/api/the-time.rb
84
86
  - example/config.ru
85
87
  - lib/phaedra.rb
86
88
  - lib/phaedra/base.rb
87
89
  - lib/phaedra/concerns/callbacks_actionable.rb
90
+ - lib/phaedra/middleware.rb
91
+ - lib/phaedra/middleware/not_found.rb
92
+ - lib/phaedra/middleware/static.rb
88
93
  - lib/phaedra/rack_app.rb
89
94
  - lib/phaedra/version.rb
90
95
  - phaedra.gemspec
@@ -107,9 +112,9 @@ required_rubygems_version: !ruby/object:Gem::Requirement
107
112
  - !ruby/object:Gem::Version
108
113
  version: '0'
109
114
  requirements: []
110
- rubygems_version: 3.0.8
115
+ rubygems_version: 3.0.6
111
116
  signing_key:
112
117
  specification_version: 4
113
- summary: Write serverless Ruby functions via a REST microframework compatible with
114
- Rack or WEBrick.
118
+ summary: Write serverless Ruby functions via a REST-like microframework compatible
119
+ with Rack or WEBrick.
115
120
  test_files: []