phaedra 0.3.0 → 0.5.0

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: f9629e11c78d2a35fa9929d69a79f57fe1711fa4c6f6b4d73e70f8be43019918
4
- data.tar.gz: 65a0caf688ea9172723fe398e29861f2c22f99df02dce2d2e34523fbbb4c929b
3
+ metadata.gz: ed786e8ee4e64448d25817144069d17839cbae1416b6d55daec427f336183101
4
+ data.tar.gz: 8d436c4e1560ca6acda2a03c5c68a86c6d82909a78cef5c4b0052d132fa9438c
5
5
  SHA512:
6
- metadata.gz: 8c54e4a03e0cbbd16c4d444215ce29102315e1be92942a9c1d3542eb6eeb948de8b78fff6f5f5913e3ef1ee2c7eae11aaac70545682bcc8f4884987cdb5c28a1
7
- data.tar.gz: 666994b4fbb211c6a3bc555ac4f663997d6ed9ae24fcae4d670c76fe6392bb1f3761206000657edaa21e51778a2ebac5dad5a0171cfa45f032d9dbd69939f7d6
6
+ metadata.gz: 54debc72ff247af26e5414dd4684ac1fe543e4390a7ad96ed8f784ab8f8c32febb4b1b95076afb3aa62c6c72de6d9f7f6cacfaca9b9a39628fe2ba71cbe46de5
7
+ data.tar.gz: 7b1489588052128fd22902674ad41c3d28fb620ca1bdeefbf5d8131651aaa240478e6ee52d4d1f2f0758c64edb13f71edb02ceece474994eb3d18fdccf546fcf
data/README.md CHANGED
@@ -53,7 +53,7 @@ Your function can support `get`, `post`, `put`, `patch`, and `delete` methods wh
53
53
 
54
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.
55
55
 
56
- ## Callbacks
56
+ ### Callbacks
57
57
 
58
58
  Functions can define `action` callbacks:
59
59
 
@@ -77,30 +77,109 @@ end
77
77
 
78
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.
79
79
 
80
- ## OpenFaaS
80
+ ### Shared Code You Only Want to Run Once
81
81
 
82
- We recommend using OpenFaaS' [ruby-http template](https://github.com/openfaas-incubator/ruby-http). It boots up a Sinatra/WEBrick server and then passes all requests along to a Handler object.
83
-
84
- In your OpenFaaS project's function folder (e.g., `testphaedra`), simply define a file `handler.rb` which will load Phaedra's default Rack app:
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.
85
83
 
86
84
  ```ruby
87
- # testphaedra/handler.rb
85
+ # api/run-it-once.rb
88
86
 
89
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
+ ```
90
96
 
91
- class Handler
92
- def run(_body, env)
93
- status, headers, body = Phaedra::RackApp.new({
94
- "root_dir" => File.join(Dir.pwd, "function")
95
- }).call(env)
97
+ ```ruby
98
+ # lib/shared_code.rb
96
99
 
97
- # The OpenFaaS ruby-http return array is backwards from Rack :/
98
- [body.join(""), headers, status]
100
+ module SharedCode
101
+ def self.run_once
102
+ @one_time ||= Time.now
99
103
  end
100
104
  end
101
105
  ```
102
106
 
103
- Next, add a YAML file that lives alongside your function folder:
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 . .
152
+
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:
104
183
 
105
184
  ```yaml
106
185
  # testphaedra.yml
@@ -111,24 +190,24 @@ provider:
111
190
  gateway: http://127.0.0.1:8080
112
191
  functions:
113
192
  testphaedra:
114
- lang: ruby-http
193
+ lang: dockerfile
115
194
  handler: ./testphaedra
116
195
  image: yourdockerusername/testphaedra:latest
117
196
  ```
118
197
 
198
+ (Replace `yourdockerusername` with your [Docker Hub](https://hub.docker.com) username.)
199
+
119
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:
120
201
 
121
202
  ```sh
122
203
  curl http://127.0.0.1:8080/function/testphaedra/api/run-me
123
-
124
- # output of the Ruby function
125
204
  ```
126
205
 
127
- In case you're wondering: yes, with Phaedra you can write multiple Ruby functions accessible via different URL paths that will all get handled by a single OpenFaaS function. Obviously you're welcome to set up multiple Phaedra projects and deploy them as separate OpenFaaS functions if you wish.
206
+ In case you're wondering: yes, with Phaedra you can write multiple Ruby functions which will be accessible via different URL pathsall 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.
128
207
 
129
- ## Rack
208
+ ### Rack
130
209
 
131
- Booting Phaedra up as Rack app is very simple. All you need is a `config.ru` file alongside your `api` folder:
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:
132
211
 
133
212
  ```ruby
134
213
  require "phaedra"
@@ -136,9 +215,38 @@ require "phaedra"
136
215
  run Phaedra::RackApp.new
137
216
  ```
138
217
 
139
- 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:
221
+
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:
140
230
 
141
- ## WEBrick
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
142
250
 
143
251
  Integrating Phaedra into a WEBrick server is pretty straightforward. Given a `server` object, it can be accomplished thusly:
144
252
 
@@ -172,6 +280,8 @@ load File.join(Dir.pwd, "api", "func.rb")
172
280
  @server.mount '/path', Handler
173
281
  ```
174
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
+
175
285
  ----
176
286
 
177
287
  ## Development
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "phaedra", path: ".."
4
+ gem "puma"
@@ -1,9 +1,9 @@
1
- require "phaedra"
1
+ require_relative "../phaedra/initializers"
2
2
 
3
3
  class PhaedraFunction < Phaedra::Base
4
4
  def get(params)
5
- response["Content-Type"] = "text/html"
6
- "<p>This is Interesting. 😁</p>"
5
+ response["Content-Type"] = "text/html; charset=utf-8"
6
+ "<p>This is Interesting. 😁 #{Phaedra.the_time}</p>"
7
7
  end
8
8
  end
9
9
 
@@ -1,3 +1,3 @@
1
- require "phaedra"
1
+ require "phaedra/rack_app"
2
2
 
3
3
  run Phaedra::RackApp.new
@@ -0,0 +1,11 @@
1
+ require "phaedra"
2
+
3
+ module Phaedra
4
+ Initializers.register self do
5
+ the_time("123")
6
+ end
7
+
8
+ def self.the_time(init = nil)
9
+ @the_time ||= "#{Time.now} + #{init}"
10
+ end
11
+ end
@@ -1,7 +1,4 @@
1
1
  require "phaedra/version"
2
-
3
2
  require "webrick"
4
- require "rack"
5
-
3
+ require "phaedra/initializers"
6
4
  require "phaedra/base"
7
- require "phaedra/rack_app"
@@ -7,6 +7,10 @@ module Phaedra
7
7
  class Base
8
8
  include CallbacksActionable
9
9
 
10
+ before_action do
11
+ Initializers.run
12
+ end
13
+
10
14
  # Used by WEBrick
11
15
  def self.get_instance(server, *options)
12
16
  self.new(server, *options)
@@ -112,7 +116,7 @@ module Phaedra
112
116
 
113
117
  def set_initial_status
114
118
  @res.status = 200
115
- @res["Content-Type"] = "application/json"
119
+ @res["Content-Type"] = "application/json; charset=utf-8"
116
120
  end
117
121
 
118
122
  def call_method_action(params)
@@ -122,8 +126,8 @@ module Phaedra
122
126
 
123
127
  def complete_response
124
128
  if @res.body.is_a?(String) && !@res["Content-Type"].start_with?("text/")
125
- @res["Content-Type"] = "text/plain"
126
- elsif @res["Content-Type"] == "application/json"
129
+ @res["Content-Type"] = "text/plain; charset=utf-8"
130
+ elsif @res["Content-Type"].start_with? "application/json"
127
131
  @res.body = @res.body.to_json
128
132
  end
129
133
  end
@@ -11,14 +11,14 @@ module Phaedra
11
11
  end
12
12
 
13
13
  module ClassMethods
14
- def before_action(*args)
15
- set_callback :action, :before, *args
14
+ def before_action(*args, &block)
15
+ set_callback :action, :before, *args, &block
16
16
  end
17
- def after_action(*args)
18
- set_callback :action, :after, *args
17
+ def after_action(*args, &block)
18
+ set_callback :action, :after, *args, &block
19
19
  end
20
- def around_action(*args)
21
- set_callback :action, :around, *args
20
+ def around_action(*args, &block)
21
+ set_callback :action, :around, *args, &block
22
22
  end
23
23
  end
24
24
  end
@@ -0,0 +1,69 @@
1
+ module Phaedra
2
+ module Initializers
3
+ Registration = Struct.new(
4
+ :origin,
5
+ :priority,
6
+ :block,
7
+ keyword_init: true
8
+ ) do
9
+ def to_s
10
+ "#{owner}:#{priority} for #{block}"
11
+ end
12
+ end
13
+
14
+ DEFAULT_PRIORITY = 20
15
+
16
+ PRIORITY_MAP = {
17
+ low: 10,
18
+ normal: 20,
19
+ high: 30,
20
+ }.freeze
21
+
22
+ # initial empty hooks
23
+ @registry = []
24
+
25
+ NotAvailable = Class.new(RuntimeError)
26
+ Uncallable = Class.new(RuntimeError)
27
+
28
+ # Ensure the priority is a Fixnum
29
+ def self.priority_value(priority)
30
+ return priority if priority.is_a?(Integer)
31
+
32
+ PRIORITY_MAP[priority] || DEFAULT_PRIORITY
33
+ end
34
+
35
+ def self.register(origin, priority: DEFAULT_PRIORITY, &block)
36
+ raise Uncallable, "Initializers must respond to :call" unless block.respond_to? :call
37
+
38
+ @registry << Registration.new(
39
+ origin: origin,
40
+ priority: priority_value(priority),
41
+ block: block
42
+ )
43
+
44
+ block
45
+ end
46
+
47
+ def self.remove(origin)
48
+ @registry.delete_if { |item| item.origin == origin }
49
+ end
50
+
51
+ def self.run(force: false)
52
+ if !@initializers_ran || force
53
+ prioritized_initializers.each do |initializer|
54
+ initializer.block.call
55
+ end
56
+ end
57
+
58
+ @initializers_ran = true
59
+ end
60
+
61
+ def self.prioritized_initializers
62
+ # sort initializers according to priority and load order
63
+ grouped_initializers = @registry.group_by(&:priority)
64
+ grouped_initializers.keys.sort.reverse.map do |priority|
65
+ grouped_initializers[priority]
66
+ end.flatten
67
+ end
68
+ end
69
+ end
@@ -1,3 +1,6 @@
1
+ require "rack"
2
+ require "phaedra"
3
+
1
4
  module Phaedra
2
5
  class Request < Rack::Request
3
6
  def query
@@ -11,7 +14,7 @@ module Phaedra
11
14
  end
12
15
 
13
16
  def body
14
- @request_body ||= get_header(Rack::RACK_INPUT).read
17
+ @request_body ||= "" + get_header(Rack::RACK_INPUT).read
15
18
  end
16
19
  end
17
20
 
@@ -0,0 +1,2 @@
1
+ require "phaedra/rack_middleware/not_found"
2
+ require "phaedra/rack_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
@@ -1,3 +1,3 @@
1
1
  module Phaedra
2
- VERSION = "0.3.0"
2
+ VERSION = "0.5.0"
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.3.0
4
+ version: 0.5.0
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-07 00:00:00.000000000 Z
11
+ date: 2020-06-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -80,13 +80,19 @@ files:
80
80
  - LICENSE.txt
81
81
  - README.md
82
82
  - Rakefile
83
+ - example/Gemfile
83
84
  - example/api/simple.rb
84
85
  - example/api/the-time.rb
85
86
  - example/config.ru
87
+ - example/phaedra/initializers.rb
86
88
  - lib/phaedra.rb
87
89
  - lib/phaedra/base.rb
88
90
  - lib/phaedra/concerns/callbacks_actionable.rb
91
+ - lib/phaedra/initializers.rb
89
92
  - lib/phaedra/rack_app.rb
93
+ - lib/phaedra/rack_middleware.rb
94
+ - lib/phaedra/rack_middleware/not_found.rb
95
+ - lib/phaedra/rack_middleware/static.rb
90
96
  - lib/phaedra/version.rb
91
97
  - phaedra.gemspec
92
98
  homepage: https://github.com/whitefusionhq/phaedra