grapeape 0.0.1
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 +7 -0
- data/.gitignore +26 -0
- data/.travis.yml +4 -0
- data/Gemfile +5 -0
- data/LICENSE +20 -0
- data/README.md +38 -0
- data/Rakefile +19 -0
- data/examples/hello_world/Gemfile +4 -0
- data/examples/hello_world/Procfile +2 -0
- data/examples/hello_world/api.rb +7 -0
- data/examples/hello_world/worker.rb +9 -0
- data/grape_ape.gemspec +39 -0
- data/lib/grape_ape.rb +15 -0
- data/lib/grape_ape/api.rb +48 -0
- data/lib/grape_ape/consumer/application.rb +66 -0
- data/lib/grape_ape/dispatcher.rb +22 -0
- data/lib/grape_ape/endpoint.rb +5 -0
- data/lib/grape_ape/goliath/application_patch.rb +47 -0
- data/lib/grape_ape/goliath/runner.rb +24 -0
- data/lib/grape_ape/goliath/server.rb +67 -0
- data/lib/grape_ape/server.rb +15 -0
- data/lib/grape_ape/test_helper.rb +41 -0
- data/lib/grape_ape/version.rb +3 -0
- data/lib/grape_ape/worker.rb +39 -0
- data/spec/api_spec.rb +52 -0
- data/spec/dispatcher_spec.rb +5 -0
- data/spec/spec_helper.rb +21 -0
- data/spec/test_helper_spec.rb +39 -0
- metadata +219 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d68ce46191052f09bdbbf499ebae1fd5bb9e48d9
|
4
|
+
data.tar.gz: 38e7d9103f7f8c389c7db49bbf1653cc6de502f6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: edb0b9d3ac677a0145999073dee62348ecd80a21692ad64a4949609d2fa0018a186384983b1bbcefd1fc996d8a5b6c77f0a49ea717433393e628f0ea5daad6b3
|
7
|
+
data.tar.gz: 555485bfe9218645b428328ecd0d3eeab1806683ecb8192b8cd1e4a99e4af11ed6f10536bc0212e83cd0085a342b5ce3ae1885f79e1bc8b93def986cb0b54da2
|
data/.gitignore
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
coverage
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
lib/bundler/man
|
9
|
+
pkg
|
10
|
+
rdoc
|
11
|
+
spec/reports
|
12
|
+
test/tmp
|
13
|
+
test/version_tmp
|
14
|
+
tmp
|
15
|
+
|
16
|
+
# YARD artifacts
|
17
|
+
.yardoc
|
18
|
+
_yardoc
|
19
|
+
doc/
|
20
|
+
|
21
|
+
# Rubymine artifacts
|
22
|
+
.idea/
|
23
|
+
|
24
|
+
# RVM
|
25
|
+
.ruby-version
|
26
|
+
.ruby-gemset
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 David Justice
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
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, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# GrapeApe
|
2
|
+
|
3
|
+
[](https://codeclimate.com/github/devigned/grapeape)
|
4
|
+
[](https://travis-ci.org/devigned/grapeape)
|
5
|
+
[](https://coveralls.io/r/devigned/grapeape?branch=master)
|
6
|
+
|
7
|
+
Eventmachine driven distributed web API in ruby. The project stands up an event driven web API
|
8
|
+
([goliath](http://postrank-labs.github.io/goliath/) / [grape](http://intridea.github.io/grape/)) backed by
|
9
|
+
[AMQP](http://www.amqp.org/). AMQP is used to route messages from the web to a collection of event driven worker
|
10
|
+
processes in an RPC flow.
|
11
|
+
|
12
|
+
Note: this is not ready for primetime
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Add this line to your application's Gemfile:
|
17
|
+
|
18
|
+
gem 'grapeape'
|
19
|
+
|
20
|
+
And then execute:
|
21
|
+
|
22
|
+
$ bundle
|
23
|
+
|
24
|
+
Or install it yourself as:
|
25
|
+
|
26
|
+
$ gem install grapeape
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
TODO: Write usage instructions here
|
31
|
+
|
32
|
+
## Contributing
|
33
|
+
|
34
|
+
1. Fork it
|
35
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
36
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
37
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
38
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'rake'
|
4
|
+
|
5
|
+
def safe_load
|
6
|
+
begin
|
7
|
+
yield
|
8
|
+
rescue LoadError => ex
|
9
|
+
puts 'Error loading rake tasks, but will continue...'
|
10
|
+
puts ex.message
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
safe_load do
|
15
|
+
require 'rspec/core/rake_task'
|
16
|
+
RSpec::Core::RakeTask.new(:spec)
|
17
|
+
end
|
18
|
+
|
19
|
+
task :default => [:spec]
|
data/grape_ape.gemspec
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'grape_ape/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.platform = Gem::Platform::RUBY
|
8
|
+
s.name = 'grapeape'
|
9
|
+
s.version = GrapeApe::VERSION
|
10
|
+
s.authors = ['David Justice']
|
11
|
+
s.email = %w(david@devigned.com)
|
12
|
+
s.description = 'Message based, event driven web dsl in ruby'
|
13
|
+
s.summary = <<-MSG
|
14
|
+
Message based, event driven web dsl in ruby. The project stands up an event driven web (goliath / grape) dsl backed by
|
15
|
+
AMQP. AMQP is used to route messages from the web to a collection of event driven worker processes in an RPC flow.
|
16
|
+
|
17
|
+
This could be used to set up a quick [CQRS](http://martinfowler.com/bliki/CQRS.html) architecture.
|
18
|
+
MSG
|
19
|
+
s.homepage = ''
|
20
|
+
s.license = 'MIT'
|
21
|
+
s.required_ruby_version = '>= 2.0.0'
|
22
|
+
|
23
|
+
s.files = `git ls-files`.split($/)
|
24
|
+
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
25
|
+
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
26
|
+
s.require_paths = %w(lib)
|
27
|
+
|
28
|
+
s.add_development_dependency 'bundler', '~> 1.3'
|
29
|
+
s.add_development_dependency 'rake'
|
30
|
+
s.add_development_dependency 'rspec'
|
31
|
+
s.add_development_dependency 'rack-test'
|
32
|
+
s.add_development_dependency 'em-http-request'
|
33
|
+
s.add_development_dependency 'coveralls'
|
34
|
+
|
35
|
+
s.add_dependency 'goliath'
|
36
|
+
s.add_dependency 'grape'
|
37
|
+
s.add_dependency 'amqp'
|
38
|
+
s.add_dependency 'activesupport'
|
39
|
+
end
|
data/lib/grape_ape.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'hashie'
|
2
|
+
require 'active_support/json'
|
3
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
4
|
+
require 'grape'
|
5
|
+
require 'grape_ape/version'
|
6
|
+
require 'amqp'
|
7
|
+
|
8
|
+
module GrapeApe
|
9
|
+
autoload :API, 'grape_ape/api'
|
10
|
+
autoload :Dispatcher, 'grape_ape/dispatcher'
|
11
|
+
autoload :Endpoint, 'grape_ape/endpoint'
|
12
|
+
autoload :Worker, 'grape_ape/worker'
|
13
|
+
autoload :Server, 'grape_ape/server'
|
14
|
+
end
|
15
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'active_support/core_ext/class/attribute'
|
2
|
+
require 'grape_ape/goliath/application_patch'
|
3
|
+
|
4
|
+
module GrapeApe
|
5
|
+
class API < Grape::API
|
6
|
+
REQUIRED_ROUTE_KEYS = [:routing_key, :method]
|
7
|
+
|
8
|
+
cattr_accessor :app_class
|
9
|
+
|
10
|
+
class << self
|
11
|
+
def inherited(subclass)
|
12
|
+
super
|
13
|
+
GrapeApe::API.app_class = subclass.name if defined?(GrapeApe::API)
|
14
|
+
end
|
15
|
+
|
16
|
+
def route(methods, paths = %w(/), route_options = {}, &block)
|
17
|
+
if ape_route?(route_options)
|
18
|
+
endpoint_options = {
|
19
|
+
:method => methods,
|
20
|
+
:path => paths,
|
21
|
+
:route_options => (@namespace_description || {}).deep_merge(@last_description || {}).deep_merge(route_options || {})
|
22
|
+
}
|
23
|
+
|
24
|
+
endpoints << GrapeApe::Endpoint.new(settings.clone, endpoint_options) do
|
25
|
+
message = rpc(env, route_options[:routing_key], {method: route_options[:method], params: params})
|
26
|
+
if block
|
27
|
+
block.call(message)
|
28
|
+
else
|
29
|
+
message
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
@last_description = nil
|
34
|
+
reset_validations!
|
35
|
+
else
|
36
|
+
super
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def ape_route?(route_options)
|
43
|
+
keys = Set.new(route_options.keys)
|
44
|
+
Set[*REQUIRED_ROUTE_KEYS].subset?(keys)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'active_support/core_ext/class/attribute'
|
2
|
+
require 'eventmachine'
|
3
|
+
require 'em-synchrony'
|
4
|
+
require 'amqp'
|
5
|
+
|
6
|
+
module GrapeApe
|
7
|
+
module Consumer
|
8
|
+
module Application
|
9
|
+
|
10
|
+
module_function
|
11
|
+
|
12
|
+
def amqp_channel
|
13
|
+
@channel
|
14
|
+
end
|
15
|
+
|
16
|
+
def register(routing_key, klass)
|
17
|
+
@keys_and_klasses ||= {}
|
18
|
+
@keys_and_klasses[routing_key] = klass
|
19
|
+
end
|
20
|
+
|
21
|
+
def run!
|
22
|
+
start do
|
23
|
+
connect
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def connect
|
28
|
+
AMQP.connect do |connection|
|
29
|
+
@channel = AMQP::Channel.new(connection)
|
30
|
+
|
31
|
+
@keys_and_klasses.each do |key, value|
|
32
|
+
q = @channel.queue(key, :auto_delete => true)
|
33
|
+
q.subscribe do |metadata, payload|
|
34
|
+
value.new.handle_message(metadata, payload)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Stops the consumer running.
|
41
|
+
def stop
|
42
|
+
logger.info('Stopping the consumers...')
|
43
|
+
EM.stop
|
44
|
+
end
|
45
|
+
|
46
|
+
def start
|
47
|
+
EM.epoll
|
48
|
+
EM.synchrony do
|
49
|
+
trap('INT') { stop }
|
50
|
+
trap('TERM') { stop }
|
51
|
+
yield if block_given?
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
at_exit do
|
56
|
+
# Only run the application if ...
|
57
|
+
# - we want it to run
|
58
|
+
# - there has been no exception raised
|
59
|
+
# - the file that has been run, is the goliath application file
|
60
|
+
if $!.nil?
|
61
|
+
Application.run!
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module GrapeApe
|
2
|
+
module Dispatcher
|
3
|
+
def rpc(env, queue_name, message = {})
|
4
|
+
f = Fiber.current
|
5
|
+
response = nil
|
6
|
+
env['subscription'] = env.grape_amqp_em_channel.subscribe do |msg|
|
7
|
+
if msg[:meta].correlation_id == env['correlation_id']
|
8
|
+
response = msg[:data]
|
9
|
+
f.resume
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
env['correlation_id'] = SecureRandom.uuid
|
14
|
+
env.grape_amqp_exchange.publish(message.to_json,
|
15
|
+
routing_key: queue_name,
|
16
|
+
reply_to: env.grape_amqp_response_queue,
|
17
|
+
correlation_id: env['correlation_id'])
|
18
|
+
Fiber.yield
|
19
|
+
JSON.parse(response)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'goliath'
|
2
|
+
require 'goliath/websocket'
|
3
|
+
require 'grape_ape'
|
4
|
+
require 'grape_ape/goliath/server'
|
5
|
+
require 'grape_ape/goliath/runner'
|
6
|
+
|
7
|
+
Goliath::Application.module_eval do
|
8
|
+
|
9
|
+
Goliath::Application::CALLERS_TO_IGNORE << /\/grape_ape\/goliath_runner.rb$/
|
10
|
+
Goliath::Application::CALLERS_TO_IGNORE << /\/ruby-debug-ide-(.+)\//
|
11
|
+
Goliath::Application::CALLERS_TO_IGNORE << /\/debase\//
|
12
|
+
|
13
|
+
module_function
|
14
|
+
|
15
|
+
def camel_case(str)
|
16
|
+
return str if str !~ /_/ && str =~ /[A-Z]+.*/
|
17
|
+
|
18
|
+
str.split('_').map { |e| e.capitalize }.join
|
19
|
+
end
|
20
|
+
|
21
|
+
alias :super_run! :run!
|
22
|
+
|
23
|
+
# Execute the application
|
24
|
+
#
|
25
|
+
# @return [Nil]
|
26
|
+
def run!
|
27
|
+
if GrapeApe::API.app_class
|
28
|
+
begin
|
29
|
+
klass = Kernel
|
30
|
+
GrapeApe::API.app_class.split('::').each { |con| klass = klass.const_get(con) }
|
31
|
+
api = GrapeApe::Server.new(api: klass)
|
32
|
+
rescue NameError
|
33
|
+
raise NameError, "Class #{@app_class} not found."
|
34
|
+
end
|
35
|
+
|
36
|
+
runner = GrapeApe::Goliath::Runner.new(ARGV, api)
|
37
|
+
runner.app = Goliath::Rack::Builder.build(GrapeApe::Server, api)
|
38
|
+
|
39
|
+
runner.load_plugins(GrapeApe::Server.plugins)
|
40
|
+
|
41
|
+
runner.run
|
42
|
+
else
|
43
|
+
super_run!
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'goliath/runner'
|
2
|
+
require 'grape_ape/goliath/server'
|
3
|
+
|
4
|
+
module GrapeApe
|
5
|
+
module Goliath
|
6
|
+
class Runner < ::Goliath::Runner
|
7
|
+
|
8
|
+
# Sets up the Goliath server
|
9
|
+
#
|
10
|
+
# @param log [Logger] The logger to configure the server to log to
|
11
|
+
# @return [Server] an instance of a Goliath server
|
12
|
+
def setup_server(log = setup_logger)
|
13
|
+
server = GrapeApe::Goliath::Server.new(@address, @port)
|
14
|
+
server.logger = log
|
15
|
+
server.app = @app
|
16
|
+
server.api = @api
|
17
|
+
server.plugins = @plugins || []
|
18
|
+
server.options = @server_options
|
19
|
+
server
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
|
3
|
+
module GrapeApe
|
4
|
+
module Goliath
|
5
|
+
class Server < ::Goliath::Server
|
6
|
+
def start(&blk)
|
7
|
+
EM.epoll
|
8
|
+
EM.synchrony do
|
9
|
+
trap('INT') { stop }
|
10
|
+
trap('TERM') { stop }
|
11
|
+
|
12
|
+
if RUBY_PLATFORM !~ /mswin|mingw/
|
13
|
+
trap('HUP') { load_config(options[:config]) }
|
14
|
+
end
|
15
|
+
|
16
|
+
load_config(options[:config])
|
17
|
+
load_plugins
|
18
|
+
setup_amqp
|
19
|
+
|
20
|
+
EM.set_effective_user(options[:user]) if options[:user]
|
21
|
+
|
22
|
+
config[::Goliath::Constants::GOLIATH_SIGNATURE] = EM.start_server(address, port, ::Goliath::Connection) do |conn|
|
23
|
+
if options[:ssl]
|
24
|
+
conn.start_tls(
|
25
|
+
:private_key_file => options[:ssl_key],
|
26
|
+
:cert_chain_file => options[:ssl_cert],
|
27
|
+
:verify_peer => options[:ssl_verify]
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
conn.port = port
|
32
|
+
conn.app = app
|
33
|
+
conn.api = api
|
34
|
+
conn.logger = logger
|
35
|
+
conn.status = status
|
36
|
+
conn.config = config
|
37
|
+
conn.options = options
|
38
|
+
end
|
39
|
+
|
40
|
+
blk.call(self) if blk
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def stop
|
45
|
+
logger.info('Stopping server...')
|
46
|
+
config['grape_amqp_conn'].close { EM.stop }
|
47
|
+
end
|
48
|
+
|
49
|
+
def setup_amqp
|
50
|
+
config['grape_amqp_conn'] = AMQP.connect(on_possible_authentication_failure: Proc.new { |settings|
|
51
|
+
logger.info "Authentication failed, as expected, settings are: #{settings.inspect}"
|
52
|
+
EM.stop
|
53
|
+
})
|
54
|
+
config['grape_amqp_channel'] = AMQP::Channel.new(config['grape_amqp_conn'])
|
55
|
+
config['grape_amqp_exchange'] = config['grape_amqp_channel'].default_exchange
|
56
|
+
config['grape_amqp_response_queue'] = SecureRandom.uuid
|
57
|
+
config['grape_amqp_em_channel'] = EM::Channel.new
|
58
|
+
|
59
|
+
q = config['grape_amqp_channel'].queue(config['grape_amqp_response_queue'], exclusive: true)
|
60
|
+
|
61
|
+
q.subscribe do |meta, payload|
|
62
|
+
config['grape_amqp_em_channel'].push({meta: meta, data: payload})
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'goliath/test_helper'
|
2
|
+
|
3
|
+
module GrapeApe
|
4
|
+
module TestHelper
|
5
|
+
include ::Goliath::TestHelper
|
6
|
+
|
7
|
+
# Wrapper for launching API and executing given code block. This
|
8
|
+
# will start the EventMachine reactor running.
|
9
|
+
#
|
10
|
+
# @param api [Class] The GrapeApe::API class to launch
|
11
|
+
# @param options [Hash] The options to pass to the server
|
12
|
+
# @param blk [Proc] The code to execute after the server is launched.
|
13
|
+
# @note This will not return until stop is called.
|
14
|
+
def with_api(api, options = {}, &blk)
|
15
|
+
server(GrapeApe::Server.new(api: api), options.delete(:port) || 9900, options, &blk)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Launches an instance of a given API server. The server
|
19
|
+
# will launch on the specified port.
|
20
|
+
#
|
21
|
+
# @param api [Class] The API class to launch
|
22
|
+
# @param port [Integer] The port to run the server on
|
23
|
+
# @param options [Hash] The options hash to provide to the server
|
24
|
+
# @return [Goliath::Server] The executed server
|
25
|
+
def server(api, port, options = {}, &blk)
|
26
|
+
op = OptionParser.new
|
27
|
+
|
28
|
+
s = GrapeApe::Goliath::Server.new
|
29
|
+
s.logger = setup_logger(options)
|
30
|
+
s.api = api
|
31
|
+
s.app = ::Goliath::Rack::Builder.build(api.class, s.api)
|
32
|
+
s.api.options_parser(op, options)
|
33
|
+
s.options = options
|
34
|
+
s.port = port
|
35
|
+
s.plugins = api.class.plugins
|
36
|
+
@test_server_port = s.port if blk
|
37
|
+
s.start(&blk)
|
38
|
+
s
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'grape_ape/consumer/application'
|
2
|
+
|
3
|
+
module GrapeApe
|
4
|
+
class Worker
|
5
|
+
class << self
|
6
|
+
|
7
|
+
attr_reader :routing_key
|
8
|
+
|
9
|
+
def routing_key(key)
|
10
|
+
@routing_key = key
|
11
|
+
Consumer::Application.register(key, self)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def handle_message(metadata, payload)
|
16
|
+
payload = JSON.parse(payload).with_indifferent_access
|
17
|
+
response = if respond_to?(payload[:method].to_sym)
|
18
|
+
begin
|
19
|
+
ok(send(payload[:method].to_sym, payload[:params]))
|
20
|
+
rescue Exception => ex
|
21
|
+
error({message: ex.message, backtrace: ex.backtrace})
|
22
|
+
end
|
23
|
+
else
|
24
|
+
error("Worker listening to routing_key: #{self.routing_key} does not have method: #{payload[:method]}")
|
25
|
+
end
|
26
|
+
Consumer::Application.amqp_channel.default_exchange.publish(response.to_json,
|
27
|
+
routing_key: metadata.reply_to,
|
28
|
+
correlation_id: metadata.correlation_id)
|
29
|
+
end
|
30
|
+
|
31
|
+
def ok(response)
|
32
|
+
[:ok, response]
|
33
|
+
end
|
34
|
+
|
35
|
+
def error(response)
|
36
|
+
[:error, response]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/spec/api_spec.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe GrapeApe::API do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
described_class.endpoints.clear
|
7
|
+
end
|
8
|
+
|
9
|
+
subject { Class.new(GrapeApe::API) }
|
10
|
+
|
11
|
+
def app;
|
12
|
+
subject
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '.route' do
|
16
|
+
let(:ape_options) { {routing_key: 'blah', method: 'hello'} }
|
17
|
+
|
18
|
+
it 'should ask if the route is an ape route' do
|
19
|
+
described_class.should_receive(:ape_route?).with(hash_including(ape_options)).and_return(true)
|
20
|
+
described_class.route(:get, '/', ape_options)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should call super route with no ape route options' do
|
24
|
+
described_class.superclass.should_receive(:route)
|
25
|
+
described_class.route(:get, '/')
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'should add an endpoint to the endpoint collection' do
|
29
|
+
expect {
|
30
|
+
described_class.route(:get, '/', ape_options)
|
31
|
+
}.to change { described_class.endpoints.count }.from(0).to(1)
|
32
|
+
end
|
33
|
+
|
34
|
+
context 'api responses' do
|
35
|
+
it 'returns the message that rpc returns with out a block provided to the dsl method' do
|
36
|
+
subject.get '/', ape_options
|
37
|
+
GrapeApe::Endpoint.any_instance.should_receive(:rpc).and_return('hello_world')
|
38
|
+
get '/'
|
39
|
+
last_response.body.should eql 'hello_world'
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'should pass the rpc message to the provided block for processing' do
|
43
|
+
subject.get '/', ape_options do |message|
|
44
|
+
'hello ' + message
|
45
|
+
end
|
46
|
+
GrapeApe::Endpoint.any_instance.should_receive(:rpc).and_return('world')
|
47
|
+
get '/'
|
48
|
+
last_response.body.should eql 'hello world'
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'support'))
|
4
|
+
|
5
|
+
require 'coveralls'
|
6
|
+
Coveralls.wear!
|
7
|
+
|
8
|
+
require 'grape_ape'
|
9
|
+
require 'rubygems'
|
10
|
+
require 'bundler'
|
11
|
+
require 'rack/test'
|
12
|
+
Bundler.setup :default, :test
|
13
|
+
|
14
|
+
Dir["#{File.dirname(__FILE__)}/support/*.rb"].each do |file|
|
15
|
+
require file
|
16
|
+
end
|
17
|
+
|
18
|
+
RSpec.configure do |config|
|
19
|
+
config.order = 'random'
|
20
|
+
config.include Rack::Test::Methods
|
21
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'grape_ape/test_helper'
|
3
|
+
|
4
|
+
describe GrapeApe::TestHelper do
|
5
|
+
include GrapeApe::TestHelper
|
6
|
+
|
7
|
+
class TestApi < GrapeApe::API
|
8
|
+
format :json
|
9
|
+
|
10
|
+
get '/' do
|
11
|
+
{hello: 'world'}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'make request to root' do
|
16
|
+
it 'should get a 200' do
|
17
|
+
with_api(TestApi) do
|
18
|
+
em_http_client = get_request path: '/'
|
19
|
+
em_http_client.response_header.status.should eq(200)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'should get json' do
|
24
|
+
with_api(TestApi) do
|
25
|
+
em_http_client = get_request path: '/'
|
26
|
+
JSON.parse(em_http_client.response).should eq({'hello' => 'world'})
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'make request to not routed path' do
|
32
|
+
it 'get a 404' do
|
33
|
+
with_api(TestApi) do
|
34
|
+
em_http_client = get_request path: '/something'
|
35
|
+
em_http_client.response_header.status.should eq(404)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
metadata
ADDED
@@ -0,0 +1,219 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: grapeape
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- David Justice
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-10-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rack-test
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: em-http-request
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: coveralls
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - '>='
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - '>='
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: goliath
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - '>='
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: grape
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - '>='
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: amqp
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - '>='
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - '>='
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: activesupport
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - '>='
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '0'
|
146
|
+
type: :runtime
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - '>='
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '0'
|
153
|
+
description: Message based, event driven web dsl in ruby
|
154
|
+
email:
|
155
|
+
- david@devigned.com
|
156
|
+
executables: []
|
157
|
+
extensions: []
|
158
|
+
extra_rdoc_files: []
|
159
|
+
files:
|
160
|
+
- .gitignore
|
161
|
+
- .travis.yml
|
162
|
+
- Gemfile
|
163
|
+
- LICENSE
|
164
|
+
- README.md
|
165
|
+
- Rakefile
|
166
|
+
- examples/hello_world/Gemfile
|
167
|
+
- examples/hello_world/Procfile
|
168
|
+
- examples/hello_world/api.rb
|
169
|
+
- examples/hello_world/worker.rb
|
170
|
+
- grape_ape.gemspec
|
171
|
+
- lib/grape_ape.rb
|
172
|
+
- lib/grape_ape/api.rb
|
173
|
+
- lib/grape_ape/consumer/application.rb
|
174
|
+
- lib/grape_ape/dispatcher.rb
|
175
|
+
- lib/grape_ape/endpoint.rb
|
176
|
+
- lib/grape_ape/goliath/application_patch.rb
|
177
|
+
- lib/grape_ape/goliath/runner.rb
|
178
|
+
- lib/grape_ape/goliath/server.rb
|
179
|
+
- lib/grape_ape/server.rb
|
180
|
+
- lib/grape_ape/test_helper.rb
|
181
|
+
- lib/grape_ape/version.rb
|
182
|
+
- lib/grape_ape/worker.rb
|
183
|
+
- spec/api_spec.rb
|
184
|
+
- spec/dispatcher_spec.rb
|
185
|
+
- spec/spec_helper.rb
|
186
|
+
- spec/test_helper_spec.rb
|
187
|
+
homepage: ''
|
188
|
+
licenses:
|
189
|
+
- MIT
|
190
|
+
metadata: {}
|
191
|
+
post_install_message:
|
192
|
+
rdoc_options: []
|
193
|
+
require_paths:
|
194
|
+
- lib
|
195
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
196
|
+
requirements:
|
197
|
+
- - '>='
|
198
|
+
- !ruby/object:Gem::Version
|
199
|
+
version: 2.0.0
|
200
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
201
|
+
requirements:
|
202
|
+
- - '>='
|
203
|
+
- !ruby/object:Gem::Version
|
204
|
+
version: '0'
|
205
|
+
requirements: []
|
206
|
+
rubyforge_project:
|
207
|
+
rubygems_version: 2.0.3
|
208
|
+
signing_key:
|
209
|
+
specification_version: 4
|
210
|
+
summary: Message based, event driven web dsl in ruby. The project stands up an event
|
211
|
+
driven web (goliath / grape) dsl backed by AMQP. AMQP is used to route messages
|
212
|
+
from the web to a collection of event driven worker processes in an RPC flow. This
|
213
|
+
could be used to set up a quick [CQRS](http://martinfowler.com/bliki/CQRS.html)
|
214
|
+
architecture.
|
215
|
+
test_files:
|
216
|
+
- spec/api_spec.rb
|
217
|
+
- spec/dispatcher_spec.rb
|
218
|
+
- spec/spec_helper.rb
|
219
|
+
- spec/test_helper_spec.rb
|