hypernova 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: