hypernova 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6154064fe73279cdc77345298bdf5739bd92acd9
4
+ data.tar.gz: 7cd5efc03841f7897c80ef0eb297a24e45d3e758
5
+ SHA512:
6
+ metadata.gz: 1c2f2306577a3f8542c77995c962a8ea713d54cb17ef06110b2bce04d9a465f8aa2eb9e037dcb2f50b21d6ab26c1838cd483c51bd78fe389425477b2ce39bef5
7
+ data.tar.gz: 8b6f89f50c0597b6f552ab575054836f513c8717effa6ec5370555900a431a9a94edd1b5f86251ef2d49f120e2972d972875ce334ee872fa4eb265a3dac43600
data/.gitignore ADDED
@@ -0,0 +1,38 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ ## Specific to RubyMotion:
14
+ .dat*
15
+ .repl_history
16
+ build/
17
+
18
+ ## Documentation cache and generated files:
19
+ /.yardoc/
20
+ /_yardoc/
21
+ /doc/
22
+ /rdoc/
23
+
24
+ ## Environment normalization:
25
+ /.bundle/
26
+ /vendor/bundle
27
+ /lib/bundler/man/
28
+
29
+ # for a library or gem, you might want to ignore these files since the code is
30
+ # intended to run in multiple environments; otherwise, check them in:
31
+ Gemfile.lock
32
+ # .ruby-version
33
+ # .ruby-gemset
34
+
35
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
36
+ .rvmrc
37
+
38
+ mystique-ruby-*.gem
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ script:
5
+ - bundle exec rspec
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in hypernova.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Airbnb
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,211 @@
1
+ # hypernova-ruby [![Build Status](https://travis-ci.org/airbnb/hypernova-ruby.svg)](https://travis-ci.org/airbnb/hypernova-ruby)
2
+
3
+ > A Ruby client for the Hypernova service
4
+
5
+ ## Getting Started
6
+
7
+ Add this line to your application’s Gemfile:
8
+
9
+ ```ruby
10
+ gem 'hypernova'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install hypernova
20
+
21
+
22
+ In Rails, create an initializer in `config/initializers/hypernova.rb`.
23
+
24
+ ```ruby
25
+ # Really basic configuration only consists of the host and the port
26
+ Hypernova.configure do |config|
27
+ config.host = "localhost"
28
+ config.port = 80
29
+ end
30
+ ```
31
+
32
+ Add an `:around_filter` to your controller so you can opt into Hypernova rendering of view partials.
33
+
34
+ ```ruby
35
+ # In my_controller.rb
36
+ require 'hypernova'
37
+
38
+ class MyController < ApplicationController
39
+ around_filter :hypernova_render_support
40
+ end
41
+ ```
42
+
43
+ Use the following methods to render React components in your view/templates.
44
+
45
+ ```erb
46
+ <%=
47
+ render_react_component(
48
+ 'MyComponent.js',
49
+ :name => 'Person',
50
+ :color => 'Blue',
51
+ :shape => 'Triangle'
52
+ )
53
+ %>
54
+ ```
55
+
56
+ ## Configuration
57
+
58
+ You can pass more configuration options to Hypernova.
59
+
60
+ ```ruby
61
+ Hypernova.configure do |config|
62
+ config.http_adapter = :patron # Use any adapter supported by Faraday
63
+ config.host = "localhost"
64
+ config.port = 80
65
+ config.open_timeout = 0.1
66
+ config.scheme = :https # Valid schemes include :http and :https
67
+ config.timeout = 0.6
68
+ end
69
+ ```
70
+
71
+ If you do not want to use `Faraday`, you can configure Hypernova Ruby to use an HTTP client that
72
+ responds to `post` and accepts a hash argument.
73
+
74
+ ```ruby
75
+ Hypernova.configure do |config|
76
+ # Use your own HTTP client!
77
+ config.http_client = SampleHTTPClient.new
78
+ end
79
+ ```
80
+
81
+ You can access a lower-level interface to exactly specify the parameters that are sent to the
82
+ Hypernova service.
83
+
84
+ ```erb
85
+ <% things.each |thing| %>
86
+ <li>
87
+ <%=
88
+ hypernova_batch_render(
89
+ :name => 'your/component/thing.bundle.js',
90
+ :data => thing
91
+ )
92
+ %>
93
+ </li>
94
+ <% end %>
95
+ ```
96
+
97
+ You can also use the batch interface if you want to create and submit batches yourself:
98
+
99
+ ```ruby
100
+ batch = Hypernova::Batch.new(service)
101
+
102
+ # each job in a hypernova render batch is identified by a token
103
+ # this allows retrieval of unordered jobs
104
+ token = batch.render(
105
+ :name => 'some_bundle.bundle.js',
106
+ :data => {foo: 1, bar: 2}
107
+ )
108
+ token2 = batch.render(
109
+ :name => 'some_bundle.bundle.js',
110
+ :data => {foo: 2, bar: 1}
111
+ )
112
+ # now we can submit the batch job and await its results
113
+ # this blocks, and takes a significant time in round trips, so try to only
114
+ # use it once per request!
115
+ result = batch.submit!
116
+
117
+ # ok now we can access our rendered strings.
118
+ foo1 = result[token].html_safe
119
+ foo2 = result[token2].html_safe
120
+ ```
121
+
122
+ ## Plugins
123
+
124
+ Hypernova enables you to control and alter requests at different stages of
125
+ the render lifecycle via a plugin system.
126
+
127
+ ### Example
128
+
129
+ All methods on a plugin are optional, and they are listed in the order that
130
+ they are called.
131
+
132
+ **initializers/hypernova.rb:**
133
+ ```ruby
134
+ # initializers/hypernova.rb
135
+ require 'hypernova'
136
+
137
+ class HypernovaPlugin
138
+ # get_view_data allows you to alter the data given to any individual
139
+ # component being rendered.
140
+ # component is the name of the component being rendered.
141
+ # data is the data being given to the component.
142
+ def get_view_data(component_name, data)
143
+ phrase_hash = data[:phrases]
144
+ data[:phrases].keys.each do |phrase_key|
145
+ phrase_hash[phrase_key] = "test phrase"
146
+ end
147
+ data
148
+ end
149
+
150
+ # prepare_request allows you to alter the request object in any way that you
151
+ # need.
152
+ # Unless manipulated by another plugin, request takes the shape:
153
+ # { 'component_name.js': { :name => 'component_name.js', :data => {} } }
154
+ def prepare_request(current_request, original_request)
155
+ current_request.keys.each do |key|
156
+ phrase_hash = req[key][:data][:phrases]
157
+ if phrase_hash.present?
158
+ phrase_hash.keys.each do |phrase_key|
159
+ phrase_hash[phrase_key] = phrase_hash[phrase_key].upcase
160
+ end
161
+ end
162
+ end
163
+ current_request
164
+ end
165
+
166
+ # send_request? allows you to determine whether a request should continue
167
+ # on to the hypernova server. Returning false prevents the request from
168
+ # occurring, and results in the fallback html.
169
+ def send_request?(request)
170
+ true
171
+ end
172
+
173
+ # after_response gives you a chance to alter the response from hypernova.
174
+ # This will be most useful for altering the resulting html field, and special
175
+ # handling of any potential errors.
176
+ # res is a Hash like { 'component_name.js': { html: String, err: Error? } }
177
+ def after_response(current_response, original_response)
178
+ current_response.keys.each do |key|
179
+ hash = current_response[key]
180
+ hash['html'] = '<div>hello</div>'
181
+ end
182
+ current_response
183
+ end
184
+
185
+ # NOTE: If an error happens in here, it won’t be caught.
186
+ def on_error(error, jobs)
187
+ puts "Oh no, error - #{error}, jobs - #{jobs}"
188
+ end
189
+ end
190
+
191
+ Hypernova.add_plugin!(HypernovaPlugin.new)
192
+ ```
193
+
194
+ ## Development
195
+
196
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
197
+ `bin/console` for an interactive prompt that will allow you to experiment.
198
+
199
+ To install this gem onto your local machine, run `bundle exec rake install`. To
200
+ release a new version, update the version number in `version.rb`, and then run
201
+ `bundle exec rake release` to create a git tag for the version, push git
202
+ commits and tags, and push the `.gem` file to
203
+ [rubygems.org](https://rubygems.org).
204
+
205
+ ## Contributing
206
+
207
+ 1. Fork it ( https://github.com/[my-github-username]/hypernova/fork )
208
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
209
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
210
+ 4. Push to the branch (`git push origin my-new-feature`)
211
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ begin
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ rescue LoadError
7
+ end
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "hypernova"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
data/hypernova.gemspec ADDED
@@ -0,0 +1,42 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "hypernova/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.authors = %w(
8
+ Jake Teton-Landis
9
+ Jordan Harband
10
+ Ian Christian Myers
11
+ Tommy Dang
12
+ )
13
+ spec.bindir = "exe"
14
+ spec.description = "A Ruby client for the Hypernova service"
15
+ spec.email = %w(
16
+ jake.tl@airbnb.com
17
+ ljharb@gmail.com
18
+ ian.myers@airbnb.com
19
+ tommy.dang@airbnb.com
20
+ )
21
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
23
+ spec.homepage = 'https://github.com/airbnb/hypernova-ruby'
24
+ spec.license = 'MIT'
25
+ spec.name = 'hypernova'
26
+ spec.require_paths = ["lib"]
27
+ spec.summary = %q{Batch interface for Hypernova, the React render service.}
28
+ spec.version = Hypernova::VERSION
29
+
30
+ if spec.respond_to?(:metadata)
31
+ spec.metadata["allowed_push_host"] = 'https://rubygems.org'
32
+ end
33
+
34
+ spec.add_development_dependency "bundler", "~> 1.9"
35
+ spec.add_development_dependency "rake", "~> 10.0"
36
+ spec.add_development_dependency "rspec", "~> 3.4"
37
+ spec.add_development_dependency "simplecov", "~> 0.11"
38
+ spec.add_development_dependency "pry", "~> 0.10"
39
+ spec.add_development_dependency "webmock", "~> 2.0"
40
+
41
+ spec.add_runtime_dependency "faraday", "~> 0.8"
42
+ end
@@ -0,0 +1,49 @@
1
+ require 'forwardable'
2
+
3
+ module Hypernova
4
+ class Batch
5
+ extend Forwardable
6
+
7
+ attr_accessor :service
8
+ attr_reader :jobs
9
+
10
+ def_delegators :jobs, :empty?
11
+
12
+ ##
13
+ # @param service the Hypernova backend service to use for render_react_batch
14
+ # The only requirement for the `service` object is the method render_react_batch
15
+ # which should accept a Hash of { job_token :: Scalar => job_data :: Hash }
16
+ # the subscript operator, to access result via tokens
17
+ def initialize(service)
18
+ # TODO: make hashmap instead????
19
+ @jobs = []
20
+ @service = service
21
+ end
22
+
23
+ def render(job)
24
+ Hypernova.verify_job_shape(job)
25
+ token = jobs.length
26
+ jobs << job
27
+ token.to_s
28
+ end
29
+
30
+ def submit!
31
+ service.render_batch(jobs_hash)
32
+ end
33
+
34
+ def submit_fallback!
35
+ service.render_batch_blank(jobs_hash)
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :service
41
+
42
+ # creates a hash with each index mapped to the value at that index
43
+ def jobs_hash
44
+ hash = {}
45
+ jobs.each_with_index { |job, idx| hash[idx.to_s] = job }
46
+ hash
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,65 @@
1
+ require "hypernova/blank_renderer"
2
+
3
+ class Hypernova::BatchRenderer
4
+ def initialize(jobs)
5
+ @jobs = jobs
6
+ end
7
+
8
+ # Sample response argument:
9
+ # {
10
+ # "DeathStarLaserComponent.js" => {
11
+ # "duration" => 17,
12
+ # "error" => nil,
13
+ # "html" => "<h1>Hello World</h1>",
14
+ # "statusCode" => 200,
15
+ # "success" => true,
16
+ # },
17
+ # "IonCannon.js" => {
18
+ # "duration" => 7,
19
+ # "error" => {
20
+ # "stack" => [
21
+ # "no_plans",
22
+ # "not_enough_resources",
23
+ # ],
24
+ # },
25
+ # "html" => blank_html_rendered_by_blank_renderer,
26
+ # "statusCode" => 500,
27
+ # "success" => false,
28
+ # },
29
+ # }
30
+
31
+ # Example of what is returned by this method:
32
+ # {
33
+ # "DeathStarLaserComponent.js" => "<h1>Hello World</h1>",
34
+ # "IonCannon.js" => <p>Feel my power!</p>,
35
+ # }
36
+ def render(response)
37
+ response.each_with_object({}) do |array, hash|
38
+ name_of_component = array[0]
39
+ hash[name_of_component] = extract_html_from_result(name_of_component, array[1])
40
+ end
41
+ end
42
+
43
+ # Example of what is returned by this method:
44
+ # {
45
+ # "DeathStarLaserComponent.js" => <div>I am blank</div>,
46
+ # "IonCannon.js" => <div>I am blank</div>,
47
+ # }
48
+ def render_blank
49
+ hash = {}
50
+ jobs.each { |name_of_component, job| hash[name_of_component] = render_blank_html(job) }
51
+ hash
52
+ end
53
+
54
+ private
55
+
56
+ attr_reader :jobs
57
+
58
+ def extract_html_from_result(name_of_component, result)
59
+ result["html"].nil? ? render_blank_html(jobs[name_of_component]) : result["html"]
60
+ end
61
+
62
+ def render_blank_html(job)
63
+ Hypernova::BlankRenderer.new(job).render
64
+ end
65
+ end
@@ -0,0 +1,13 @@
1
+ require "uri"
2
+
3
+ class Hypernova::BatchUrlBuilder
4
+ def self.base_url
5
+ configuration = Hypernova.configuration
6
+ builder = configuration.scheme == :https ? URI::HTTPS : URI::HTTP
7
+ builder.build(host: configuration.host, port: configuration.port).to_s
8
+ end
9
+
10
+ def self.path
11
+ "/batch"
12
+ end
13
+ end
@@ -0,0 +1,34 @@
1
+ require "json"
2
+
3
+ class Hypernova::BlankRenderer
4
+ def initialize(job)
5
+ @job = job
6
+ end
7
+
8
+ def render
9
+ <<-HTML
10
+ <div data-hypernova-key="#{key}"></div>
11
+ <script type="application/json" data-hypernova-key="#{key}"><!--#{encode}--></script>
12
+ HTML
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :job
18
+
19
+ def data
20
+ job[:data]
21
+ end
22
+
23
+ def encode
24
+ JSON.generate(data).gsub(/&/, '&amp;').gsub(/>/, '&gt;')
25
+ end
26
+
27
+ def key
28
+ name.gsub(/\W/, "")
29
+ end
30
+
31
+ def name
32
+ job[:name]
33
+ end
34
+ end
@@ -0,0 +1,34 @@
1
+ require "faraday"
2
+
3
+ class Hypernova::Configuration
4
+ VALID_SCHEMES = [:http, :https].freeze
5
+
6
+ attr_accessor :http_adapter,
7
+ :http_client,
8
+ :host,
9
+ :open_timeout,
10
+ :port,
11
+ :scheme,
12
+ :timeout
13
+
14
+ def initialize
15
+ @open_timeout = 0.1
16
+ @scheme = :http
17
+ @timeout = 0.6
18
+ end
19
+
20
+ def http_adapter
21
+ @http_adapter || Faraday.default_adapter
22
+ end
23
+
24
+ def scheme=(value)
25
+ validate_scheme!(value)
26
+ @scheme = value
27
+ end
28
+
29
+ private
30
+
31
+ def validate_scheme!(value)
32
+ raise TypeError.new("Unknown scheme #{value}") unless VALID_SCHEMES.include?(value)
33
+ end
34
+ end
@@ -0,0 +1,106 @@
1
+ require "hypernova/plugin_helper"
2
+ require "hypernova/request_service"
3
+
4
+ module Hypernova
5
+ ##
6
+ # Mixin.
7
+ # Implements the high-level rails helper interface.
8
+ # Currently untested.
9
+ module ControllerHelpers
10
+ include Hypernova::PluginHelper
11
+
12
+ ##
13
+ # a Rails around_filter to support hypernova batch rendering.
14
+ def hypernova_render_support
15
+ hypernova_batch_before
16
+ yield
17
+ hypernova_batch_after
18
+ end
19
+
20
+ ##
21
+ # enqueue a render into the current request's hypernova batch
22
+ def hypernova_batch_render(job)
23
+ if @hypernova_batch.nil?
24
+ raise NilBatchError.new('called hypernova_batch_render without calling '\
25
+ 'hypernova_batch_before. Check your around_filter for :hypernova_render_support')
26
+ end
27
+ batch_token = @hypernova_batch.render(job)
28
+ template_safe_token = Hypernova.render_token(batch_token)
29
+ @hypernova_batch_mapping[template_safe_token] = batch_token
30
+ template_safe_token
31
+ end
32
+
33
+ ##
34
+ # shortcut method to render a react component
35
+ # @param [String] name the hypernova bundle name, like 'packages/p3/foo.bundle.js' (for now)
36
+ # @param [Hash] props the props to be passed to the component
37
+ # :^)k|8 <-- this is a chill peep riding a skateboard
38
+ def render_react_component(component, data = {})
39
+ begin
40
+ new_data = get_view_data(component, data)
41
+ rescue StandardError => e
42
+ on_error(e)
43
+ new_data = data
44
+ end
45
+ job = {
46
+ :data => new_data,
47
+ :name => component,
48
+ }
49
+
50
+ hypernova_batch_render(job)
51
+ end
52
+
53
+ ##
54
+ # Retrieve a handle to a hypernova service
55
+ # OVERRIDE IN YOUR IMPLEMENTATION CLASS TO GET A DIFFERENT SERVICE
56
+ def hypernova_service
57
+ @_hypernova_service ||= Hypernova::RequestService.new
58
+ end
59
+
60
+ private
61
+
62
+ ##
63
+ # set up a new hypernova batch for this request.
64
+ # The batch's service is provided by instance method #hypernova_service
65
+ # which you should override after including this mixin.
66
+ def hypernova_batch_before
67
+ @hypernova_batch = Hypernova::Batch.new(hypernova_service)
68
+ @hypernova_batch_mapping = {}
69
+ end
70
+
71
+ ##
72
+ # Modifies response.body to have all batched hypernova render results
73
+ def hypernova_batch_after
74
+ if @hypernova_batch.nil?
75
+ raise NilBatchError.new('called hypernova_batch_after without calling '\
76
+ 'hypernova_batch_before. Check your around_filter for :hypernova_render_support')
77
+ end
78
+ return if @hypernova_batch.empty?
79
+
80
+ jobs = @hypernova_batch.jobs
81
+ hash = jobs.each_with_object({}) do |job, h|
82
+ h[job[:name]] = job
83
+ end
84
+ hash = prepare_request(hash, hash)
85
+ if send_request?(hash)
86
+ begin
87
+ will_send_request(hash)
88
+ result = @hypernova_batch.submit!
89
+ on_success(result, hash)
90
+ rescue StandardError => e
91
+ on_error(e)
92
+ result = @hypernova_batch.submit_fallback!
93
+ end
94
+ else
95
+ result = @hypernova_batch.submit_fallback!
96
+ end
97
+
98
+ new_body = Hypernova.replace_tokens_with_result(
99
+ response.body,
100
+ @hypernova_batch_mapping,
101
+ result
102
+ )
103
+ response.body = new_body
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,17 @@
1
+ require "faraday"
2
+ require "hypernova/batch_url_builder"
3
+
4
+ class Hypernova::FaradayConnection
5
+ def self.build
6
+ configuration = Hypernova.configuration
7
+ Faraday.new(
8
+ request: {
9
+ open_timeout: configuration.open_timeout,
10
+ timeout: configuration.timeout,
11
+ },
12
+ url: Hypernova::BatchUrlBuilder.base_url,
13
+ ) do |builder|
14
+ builder.adapter(configuration.http_adapter) if configuration.http_adapter
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ require "hypernova/faraday_connection"
2
+
3
+ class Hypernova::FaradayRequest
4
+ def self.post(payload)
5
+ Hypernova::FaradayConnection.build.post do |request|
6
+ request.url(Hypernova::BatchUrlBuilder.path)
7
+ request.headers["Content-Type"] = "application/json"
8
+ request.body = payload[:body].to_json
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,21 @@
1
+ require "hypernova/batch_url_builder"
2
+
3
+ class Hypernova::HttpClientRequest
4
+ def self.post(payload)
5
+ if is_client_requiring_1_argument?
6
+ client.post(Hypernova::BatchUrlBuilder.path, payload)
7
+ else
8
+ client.post(payload)
9
+ end
10
+ end
11
+
12
+ def self.client
13
+ Hypernova.configuration.http_client
14
+ end
15
+
16
+ def self.is_client_requiring_1_argument?
17
+ client.method(:post).arity == -2
18
+ end
19
+
20
+ private_class_method :client, :is_client_requiring_1_argument?
21
+ end
@@ -0,0 +1,24 @@
1
+ require "hypernova/request"
2
+ require "hypernova/response"
3
+
4
+ class Hypernova::ParsedResponse
5
+ def initialize(jobs)
6
+ @jobs = jobs
7
+ end
8
+
9
+ def body
10
+ response.parsed_body
11
+ end
12
+
13
+ private
14
+
15
+ attr_reader :jobs
16
+
17
+ def request
18
+ Hypernova::Request.new(jobs)
19
+ end
20
+
21
+ def response
22
+ Hypernova::Response.new(request)
23
+ end
24
+ end
@@ -0,0 +1,59 @@
1
+ module Hypernova::PluginHelper
2
+ def get_view_data(name, data)
3
+ Hypernova.plugins.reduce(data) do |data, plugin|
4
+ if plugin.respond_to?(:get_view_data)
5
+ plugin.get_view_data(name, data)
6
+ else
7
+ data
8
+ end
9
+ end
10
+ end
11
+
12
+ def prepare_request(current_request, original_request)
13
+ Hypernova.plugins.reduce(current_request) do |req, plugin|
14
+ if plugin.respond_to?(:prepare_request)
15
+ plugin.prepare_request(req, original_request)
16
+ else
17
+ req
18
+ end
19
+ end
20
+ end
21
+
22
+ def send_request?(jobs_hash)
23
+ Hypernova.plugins.all? do |plugin|
24
+ if plugin.respond_to?(:send_request?)
25
+ plugin.send_request?(jobs_hash)
26
+ else
27
+ true
28
+ end
29
+ end
30
+ end
31
+
32
+ def will_send_request(jobs_hash)
33
+ Hypernova.plugins.each do |plugin|
34
+ if plugin.respond_to?(:will_send_request)
35
+ plugin.will_send_request(jobs_hash)
36
+ end
37
+ end
38
+ end
39
+
40
+ def after_response(current_response, original_response)
41
+ Hypernova.plugins.reduce(current_response) do |response, plugin|
42
+ if plugin.methods.include?(:after_response)
43
+ plugin.after_response(response, original_response)
44
+ else
45
+ response
46
+ end
47
+ end
48
+ end
49
+
50
+ def on_error(error, job = {})
51
+ Hypernova.plugins.each { |plugin| plugin.on_error(error, job) if plugin.respond_to?(:on_error) }
52
+ end
53
+
54
+ def on_success(res, jobs_hash)
55
+ Hypernova.plugins.each do |plugin|
56
+ plugin.on_success(res, jobs_hash) if plugin.respond_to?(:on_success)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,27 @@
1
+ class DevelopmentModePlugin
2
+ def after_response(current_response, _)
3
+ current_response.each do |name, result|
4
+ current_response[name] = result.merge({ "html" => render(name, result) }) if result["error"]
5
+ end
6
+ end
7
+
8
+ private
9
+
10
+ def render(name, result)
11
+ <<-HTML
12
+ <div style="background-color: #ff5a5f; color: #fff; padding: 12px;">
13
+ <p style="margin: 0">
14
+ <strong>Development Warning!</strong>
15
+ The <code>#{name}</code> component failed to render with Hypernova. Error stack:
16
+ </p>
17
+ <ul style="padding: 0 20px">
18
+ <li>#{stack_trace(result).join("</li><li>")}</li>
19
+ </ul>
20
+ </div>
21
+ HTML
22
+ end
23
+
24
+ def stack_trace(result)
25
+ result["error"]["stack"] || []
26
+ end
27
+ end
@@ -0,0 +1,9 @@
1
+ require "hypernova/controller_helpers"
2
+
3
+ if defined?(ActionController::Base)
4
+ ActionController::Base.class_eval do
5
+ include Hypernova::ControllerHelpers
6
+
7
+ helper_method :render_react_component
8
+ end
9
+ end
@@ -0,0 +1,33 @@
1
+ require "hypernova/faraday_request"
2
+ require "hypernova/http_client_request"
3
+
4
+ class Hypernova::Request
5
+ def initialize(jobs)
6
+ @jobs = jobs
7
+ end
8
+
9
+ def body
10
+ post.body
11
+ end
12
+
13
+ private
14
+
15
+ attr_reader :jobs
16
+
17
+ def payload
18
+ {
19
+ :body => jobs,
20
+ :idempotent => true,
21
+ :request_format => :json,
22
+ :response_format => :json,
23
+ }
24
+ end
25
+
26
+ def post
27
+ if Hypernova.configuration.http_client
28
+ Hypernova::HttpClientRequest.post(payload)
29
+ else
30
+ Hypernova::FaradayRequest.post(payload)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,34 @@
1
+ require "hypernova/batch_renderer"
2
+ require "hypernova/parsed_response"
3
+ require "hypernova/plugin_helper"
4
+
5
+ class Hypernova::RequestService
6
+ include Hypernova::PluginHelper
7
+
8
+ def render_batch(jobs)
9
+ return render_batch_blank(jobs) if jobs.empty?
10
+ response_body = Hypernova::ParsedResponse.new(jobs).body
11
+ response_body.each do |index_string, resp|
12
+ error(resp["error"], jobs[index_string.to_i]) if resp["error"]
13
+ end
14
+ build_renderer(jobs).render(response_body)
15
+ end
16
+
17
+ def render_batch_blank(jobs)
18
+ build_renderer(jobs).render_blank
19
+ end
20
+
21
+ private
22
+
23
+ def build_error(name, message)
24
+ Module.const_get(name).new(message)
25
+ end
26
+
27
+ def build_renderer(jobs)
28
+ Hypernova::BatchRenderer.new(jobs)
29
+ end
30
+
31
+ def error(error_data, job)
32
+ on_error(build_error(error_data["name"], error_data["message"]), job)
33
+ end
34
+ end
@@ -0,0 +1,64 @@
1
+ require "json"
2
+ require "hypernova/plugin_helper"
3
+
4
+ class Hypernova::Response
5
+ include Hypernova::PluginHelper
6
+
7
+ def initialize(request)
8
+ @request = request
9
+ end
10
+
11
+ # Example parsed body with no error:
12
+ # {
13
+ # "0" => {
14
+ # "name" => "hello_world.js",
15
+ # "html" => "<div>Hello World</div>",
16
+ # "meta" => {},
17
+ # "duration" => 100,
18
+ # "statusCode" => 200,
19
+ # "success" => true,
20
+ # "error" => nil,
21
+ # }
22
+ # }
23
+
24
+ # Example parsed body with error:
25
+ # {
26
+ # "0" => {
27
+ # "html" => "<p>Error!</p>",
28
+ # "name" => "goodbye_galaxy.js",
29
+ # "meta" => {},
30
+ # "duration" => 100,
31
+ # "statusCode" => 500,
32
+ # "success" => false,
33
+ # "error" => {
34
+ # "name" => "TypeError",
35
+ # "message" => "Cannot read property 'forEach' of undefined",
36
+ # "stack" => [
37
+ # "TypeError: Cannot read property 'forEach' of undefined",
38
+ # "at TravelerLanding.componentWillMount",
39
+ # "at ReactCompositeComponentMixin.mountComponent",
40
+ # ],
41
+ # },
42
+ # }
43
+ # }
44
+ def parsed_body
45
+ response = parse_body
46
+ # This enables backward compatibility with the old server response format.
47
+ # In the new format, the response results are contained within a "results" key. The top level
48
+ # hash contains a "success" and "error" which relates to the whole batch.
49
+ response = response["results"] || response
50
+ after_response(response, response)
51
+ end
52
+
53
+ private
54
+
55
+ attr_reader :request
56
+
57
+ def body
58
+ request.body
59
+ end
60
+
61
+ def parse_body
62
+ JSON.parse(body)
63
+ end
64
+ end
@@ -0,0 +1,3 @@
1
+ module Hypernova
2
+ VERSION = "1.0.0"
3
+ end
data/lib/hypernova.rb ADDED
@@ -0,0 +1,66 @@
1
+ require "hypernova/batch"
2
+ require "hypernova/configuration"
3
+ require "hypernova/rails/action_controller"
4
+ require "hypernova/version"
5
+
6
+ module Hypernova
7
+ # thrown by ControllerHelper methods if you don't call hypernova_batch_before first
8
+ class NilBatchError < StandardError; end
9
+
10
+ # thrown by Batch#render if your job doesn't have the right keys and stuff.
11
+ class BadJobError < StandardError; end
12
+
13
+ class << self
14
+ attr_accessor :configuration
15
+ end
16
+
17
+ def self.configure
18
+ self.configuration ||= Hypernova::Configuration.new
19
+ yield(configuration)
20
+ end
21
+
22
+ # TODO: more interesting token format?
23
+ RENDER_TOKEN_REGEX = /__hypernova_render_token\[\w+\]__/
24
+
25
+ def self.render_token(batch_token)
26
+ "__hypernova_render_token[#{batch_token}]__"
27
+ end
28
+
29
+ def self.plugins
30
+ @plugins ||= []
31
+ end
32
+
33
+ def self.add_plugin!(plugin)
34
+ plugins << plugin
35
+ end
36
+
37
+ ##
38
+ # replace all hypernova tokens in `body` with the render results given by batch_result,
39
+ # using render_token_to_batch_token to map render tokens into batch tokens
40
+ # @param [String] body
41
+ # @param [Hash] render_token_to_batch_token
42
+ # @param respond_to(:[]) batch_result
43
+ def self.replace_tokens_with_result(body, render_token_to_batch_token, batch_result)
44
+ # replace all render tokens in the current response body with the
45
+ # hypernova result for that render.
46
+ return body.gsub(RENDER_TOKEN_REGEX) do |render_token|
47
+ batch_token = render_token_to_batch_token[render_token]
48
+ if batch_token.nil?
49
+ next render_token
50
+ end
51
+ render_result = batch_result[batch_token]
52
+ # replace with that render result.
53
+ next render_result
54
+ end
55
+ end
56
+
57
+ ##
58
+ # raises a BadJobError if the job hash is not of the right shape.
59
+ def self.verify_job_shape(job)
60
+ [:name, :data].each do |key|
61
+ if job[key].nil?
62
+ raise BadJobError.new("Hypernova render jobs must have key #{key}")
63
+ end
64
+ end
65
+ end
66
+ end
metadata ADDED
@@ -0,0 +1,160 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hypernova
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jake
8
+ - Teton-Landis
9
+ - Jordan
10
+ - Harband
11
+ - Ian
12
+ - Christian
13
+ - Myers
14
+ - Tommy
15
+ - Dang
16
+ autorequire:
17
+ bindir: exe
18
+ cert_chain: []
19
+
20
+ date: 2016-06-08 00:00:00 Z
21
+ dependencies:
22
+ - !ruby/object:Gem::Dependency
23
+ name: bundler
24
+ prerelease: false
25
+ requirement: &id001 !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: "1.9"
30
+ type: :development
31
+ version_requirements: *id001
32
+ - !ruby/object:Gem::Dependency
33
+ name: rake
34
+ prerelease: false
35
+ requirement: &id002 !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ~>
38
+ - !ruby/object:Gem::Version
39
+ version: "10.0"
40
+ type: :development
41
+ version_requirements: *id002
42
+ - !ruby/object:Gem::Dependency
43
+ name: rspec
44
+ prerelease: false
45
+ requirement: &id003 !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ~>
48
+ - !ruby/object:Gem::Version
49
+ version: "3.4"
50
+ type: :development
51
+ version_requirements: *id003
52
+ - !ruby/object:Gem::Dependency
53
+ name: simplecov
54
+ prerelease: false
55
+ requirement: &id004 !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ~>
58
+ - !ruby/object:Gem::Version
59
+ version: "0.11"
60
+ type: :development
61
+ version_requirements: *id004
62
+ - !ruby/object:Gem::Dependency
63
+ name: pry
64
+ prerelease: false
65
+ requirement: &id005 !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: "0.10"
70
+ type: :development
71
+ version_requirements: *id005
72
+ - !ruby/object:Gem::Dependency
73
+ name: webmock
74
+ prerelease: false
75
+ requirement: &id006 !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ~>
78
+ - !ruby/object:Gem::Version
79
+ version: "2.0"
80
+ type: :development
81
+ version_requirements: *id006
82
+ - !ruby/object:Gem::Dependency
83
+ name: faraday
84
+ prerelease: false
85
+ requirement: &id007 !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ~>
88
+ - !ruby/object:Gem::Version
89
+ version: "0.8"
90
+ type: :runtime
91
+ version_requirements: *id007
92
+ description: A Ruby client for the Hypernova service
93
+ email:
94
+ - jake.tl@airbnb.com
95
+ - ljharb@gmail.com
96
+ - ian.myers@airbnb.com
97
+ - tommy.dang@airbnb.com
98
+ executables: []
99
+
100
+ extensions: []
101
+
102
+ extra_rdoc_files: []
103
+
104
+ files:
105
+ - .gitignore
106
+ - .travis.yml
107
+ - Gemfile
108
+ - LICENSE
109
+ - README.md
110
+ - Rakefile
111
+ - bin/console
112
+ - bin/setup
113
+ - hypernova.gemspec
114
+ - lib/hypernova.rb
115
+ - lib/hypernova/batch.rb
116
+ - lib/hypernova/batch_renderer.rb
117
+ - lib/hypernova/batch_url_builder.rb
118
+ - lib/hypernova/blank_renderer.rb
119
+ - lib/hypernova/configuration.rb
120
+ - lib/hypernova/controller_helpers.rb
121
+ - lib/hypernova/faraday_connection.rb
122
+ - lib/hypernova/faraday_request.rb
123
+ - lib/hypernova/http_client_request.rb
124
+ - lib/hypernova/parsed_response.rb
125
+ - lib/hypernova/plugin_helper.rb
126
+ - lib/hypernova/plugins/development_mode_plugin.rb
127
+ - lib/hypernova/rails/action_controller.rb
128
+ - lib/hypernova/request.rb
129
+ - lib/hypernova/request_service.rb
130
+ - lib/hypernova/response.rb
131
+ - lib/hypernova/version.rb
132
+ homepage: https://github.com/airbnb/hypernova-ruby
133
+ licenses:
134
+ - MIT
135
+ metadata:
136
+ allowed_push_host: https://rubygems.org
137
+ post_install_message:
138
+ rdoc_options: []
139
+
140
+ require_paths:
141
+ - lib
142
+ required_ruby_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - &id008
145
+ - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: "0"
148
+ required_rubygems_version: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - *id008
151
+ requirements: []
152
+
153
+ rubyforge_project:
154
+ rubygems_version: 2.4.7
155
+ signing_key:
156
+ specification_version: 4
157
+ summary: Batch interface for Hypernova, the React render service.
158
+ test_files: []
159
+
160
+ has_rdoc: