magellan-rails 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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +37 -0
  3. data/Gemfile +7 -0
  4. data/Gemfile.lock +76 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +29 -0
  7. data/Rakefile +7 -0
  8. data/bin/magellan-rails +7 -0
  9. data/gems/magellan-publisher/.gitignore +37 -0
  10. data/gems/magellan-publisher/Gemfile +11 -0
  11. data/gems/magellan-publisher/Gemfile.lock +51 -0
  12. data/gems/magellan-publisher/LICENSE.txt +22 -0
  13. data/gems/magellan-publisher/README.md +29 -0
  14. data/gems/magellan-publisher/Rakefile +14 -0
  15. data/gems/magellan-publisher/lib/magellan/publisher/version.rb +5 -0
  16. data/gems/magellan-publisher/lib/magellan/publisher.rb +37 -0
  17. data/gems/magellan-publisher/magellan-publisher.gemspec +27 -0
  18. data/gems/magellan-publisher/spec/magellan/publisher_spec.rb +39 -0
  19. data/gems/magellan-publisher/spec/spec_helper.rb +8 -0
  20. data/lib/generators/install_generator.rb +20 -0
  21. data/lib/generators/templates/Magellan.yml +115 -0
  22. data/lib/magellan/extentions/rails/engine.rb +28 -0
  23. data/lib/magellan/extentions/rails.rb +2 -0
  24. data/lib/magellan/extentions.rb +2 -0
  25. data/lib/magellan/rails/executor.rb +35 -0
  26. data/lib/magellan/rails/railtie.rb +12 -0
  27. data/lib/magellan/rails/request.rb +111 -0
  28. data/lib/magellan/rails/response.rb +35 -0
  29. data/lib/magellan/rails/version.rb +5 -0
  30. data/lib/magellan/rails.rb +10 -0
  31. data/lib/magellan/subscriber/base.rb +26 -0
  32. data/lib/magellan/subscriber/executor.rb +21 -0
  33. data/lib/magellan/subscriber/mapper.rb +105 -0
  34. data/lib/magellan/subscriber/request.rb +25 -0
  35. data/lib/magellan/subscriber/rspec.rb +7 -0
  36. data/lib/magellan/subscriber/testing/integration.rb +22 -0
  37. data/lib/magellan/subscriber.rb +10 -0
  38. data/lib/magellan/worker/config.rb +33 -0
  39. data/lib/magellan/worker/core.rb +70 -0
  40. data/lib/magellan/worker/executor.rb +38 -0
  41. data/lib/magellan/worker.rb +14 -0
  42. data/lib/magellan.rb +28 -0
  43. data/magellan-rails.gemspec +28 -0
  44. data/spec/magellan/rails/request_spec.rb +103 -0
  45. data/spec/magellan/rails/response_spec.rb +58 -0
  46. data/spec/magellan/subscriber/mapper_spec.rb +264 -0
  47. data/spec/magellan/worker/core_spec.rb +70 -0
  48. data/spec/spec_helper.rb +4 -0
  49. metadata +167 -0
@@ -0,0 +1,21 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'magellan/rails'
3
+
4
+ ActiveSupport::Dependencies.autoload_paths << Rails.root.join("app/subscribers").to_s
5
+
6
+ class Magellan::Subscriber::Executor
7
+ def initialize
8
+ @map = Magellan::Subscriber::Mapper.new
9
+ end
10
+
11
+ def execute(request_message)
12
+ request = Magellan::Subscriber::Request.new()
13
+ request.parse_message(request_message)
14
+
15
+ # ロード済み定数をクリア
16
+ # TODO: 更新時刻をチェックする
17
+ ActiveSupport::Dependencies.clear
18
+
19
+ @map.call(request)
20
+ end
21
+ end
@@ -0,0 +1,105 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'magellan/rails'
3
+
4
+ class Magellan::Subscriber::RoutingError < StandardError
5
+ end
6
+
7
+ class Magellan::Subscriber::Mapper
8
+ def initialize(map_file_path=Rails.root.join("config/subscribes.yml"))
9
+ # テストのため nil で初期化できるようにしています
10
+ if map_file_path
11
+ @config_file = map_file_path.to_s
12
+ @config_file_size = nil
13
+ @config_file_mtime = nil
14
+ load_mapper_file
15
+ end
16
+ end
17
+
18
+ def load_mapper_file
19
+ if @config_file
20
+ st = File.stat(@config_file)
21
+ if st.size != @config_file_size or st.mtime > @config_file_mtime
22
+ @config = YAML.load(File.read(@config_file))
23
+ self.map = @config
24
+ end
25
+ end
26
+ end
27
+
28
+ def topic_regexp(topic)
29
+ # "#" は特別に任意の1文字以上のトピックにマッチするとする(MQTT の仕様上空文字列はトピックとして不正)
30
+ return /.+/ if topic == "#"
31
+
32
+ words = topic.split("/", -1) # 末尾の a/ を ["a", ""] と分離するため第2引数の-1が必要
33
+ # 前処理ととして連続している "#" は冗長なので1つに正規化する
34
+ words.each_cons(2).each_with_index{|(w1, w2), idx| words[idx] = nil if w1 == "#" and w2 == "#" }
35
+ words.compact!
36
+ first, *remain = words
37
+ optional_sep = false
38
+ # 先頭の要素を正規表現文字列にする
39
+ regexp_str = "\\A"
40
+ case first
41
+ when "#"
42
+ # 先頭が "#" の場合 "#/a" は "a" にもマッチするので後続の要素の "/" の正規表現はここで追加する
43
+ optional_sep = true
44
+ # #/〜 の場合 # はなくてもマッチするので、空文字列もしくは任意のword+"/"とする
45
+ regexp_str += "(?:.*/|)"
46
+ when "+"
47
+ regexp_str += "[^/]*"
48
+ else
49
+ regexp_str += Regexp.escape(first)
50
+ end
51
+ remain.each do |w|
52
+ case w
53
+ when "#"
54
+ # "〜/#" の場合 "/" はなくてもマッチする(# はレベル0でもマッチする)ので、空文字列もしくは"/"+任意のwordとする
55
+ regexp_str += "(?:|/.*)"
56
+ when "+"
57
+ regexp_str += "/" unless optional_sep
58
+ regexp_str += "[^/]*"
59
+ else
60
+ regexp_str += "/" unless optional_sep
61
+ regexp_str += Regexp.escape(w)
62
+ end
63
+ optional_sep = false
64
+ end
65
+ regexp_str += "\\z"
66
+ Regexp.compile(regexp_str)
67
+ end
68
+ private :topic_regexp
69
+
70
+ def map=(config)
71
+ @map = config.each_with_object({}) do |(key, value), tbl|
72
+ regexp = topic_regexp(key)
73
+ controller, action = value.split("#", 2)
74
+ controller_name = (controller.classify + "Subscriber")
75
+ tbl[regexp] = [controller_name, action]
76
+ end
77
+ end
78
+
79
+ def map
80
+ if Rails.env.development?
81
+ load_mapper_file
82
+ end
83
+ @map
84
+ end
85
+
86
+ def map_action(topic)
87
+ self.map.select do |re, _|
88
+ re.match(topic)
89
+ end.map do |_, (controller_name, action)|
90
+ [controller_name, action]
91
+ end
92
+ end
93
+
94
+ def call(request)
95
+ actions = map_action(request.topic)
96
+ actions.each do |controller_name, action|
97
+ controller_class = ActiveSupport::Dependencies.constantize(controller_name)
98
+ controller = controller_class.new(request)
99
+ return controller.process_action(action)
100
+ end
101
+ if actions.empty?
102
+ raise Magellan::Subscriber::RoutingError, "topic #{request.topic} has no matched subscriber"
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,25 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'magellan/rails'
3
+ require 'set'
4
+
5
+ class Magellan::Subscriber::Request
6
+ attr_reader :headers, :options, :topic, :body
7
+
8
+ def option(key)
9
+ options[key]
10
+ end
11
+
12
+ def parse_message(request_message)
13
+ # @options = request_message['options']
14
+ # TRから送られるメッセージのoptionsの型が本来はHash型なのですが、配列型になってしまっているため、暫定的に空のHashをセットします
15
+ @options = {}
16
+ @headers = request_message['headers']
17
+ @topic = request_message["headers"]["Path-Info"]
18
+ case request_message["body_encoding"]
19
+ when /\Aplain\z/i, nil
20
+ @body = request_message["body"]
21
+ when /\Abase64\z/i
22
+ @body = Base64.decode64(request_message['body'])
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,7 @@
1
+ require "magellan/subscriber/testing/integration"
2
+
3
+ require "rspec/rails/example/request_example_group"
4
+
5
+ module RSpec::Rails::RequestExampleGroup
6
+ include Magellan::Subscriber::Testing::Integration
7
+ end
@@ -0,0 +1,22 @@
1
+ # coding: utf-8
2
+
3
+ require "base64"
4
+ require "magellan/subscriber"
5
+
6
+ module Magellan::Subscriber::Testing
7
+ module Integration
8
+ def publish(topic, body="")
9
+ executor = Magellan::Subscriber::Executor.new
10
+ request_message = {
11
+ "headers" => {
12
+ "Method" => "PUBLISH",
13
+ "Path-Info" => topic,
14
+ },
15
+ "body" => Base64.strict_encode64(body),
16
+ "body_encoding" => "base64",
17
+ }
18
+ executor.execute(request_message)
19
+ end
20
+ end
21
+ end
22
+
@@ -0,0 +1,10 @@
1
+ # -*- coding: utf-8 -*-
2
+ require "magellan"
3
+ require "magellan/rails"
4
+
5
+ module Magellan::Subscriber
6
+ autoload :Executor, "magellan/subscriber/executor"
7
+ autoload :Request, "magellan/subscriber/request"
8
+ autoload :Mapper, "magellan/subscriber/mapper"
9
+ autoload :Base, "magellan/subscriber/base"
10
+ end
@@ -0,0 +1,33 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'magellan/rails'
3
+ require 'yaml'
4
+
5
+ module Magellan::Worker::Config
6
+ class << self
7
+
8
+ def parse_boolean(val, default_value=false)
9
+ case val
10
+ when /\A1\z/, /\Atrue\z/i, /\Ayes\z/i, /\Aon\z/
11
+ true
12
+ when /\A0\z/, /\Afalse\z/i, /\Ano\z/i, /\Aoff\z/
13
+ false
14
+ else
15
+ default_value
16
+ end
17
+ end
18
+
19
+ def load_config()
20
+ config = {}
21
+ config[:host] = ENV['RABBITMQ_PORT_5672_TCP_ADDR']
22
+ config[:port] = ENV['RABBITMQ_PORT_5672_TCP_PORT']
23
+ config[:vhost] = ENV['VHOST']
24
+ config[:rabbitmq_user] = ENV['RABBITMQ_USER']
25
+ config[:rabbitmq_password] = ENV['RABBITMQ_PASS']
26
+ config[:request_queue] = ENV['REQUEST_QUEUE']
27
+ config[:response_exchange] = ENV['RESPONSE_EXCHANGE']
28
+ config[:http_worker] = parse_boolean(ENV['MAGELLAN_HTTP_WORKER'], true)
29
+ config[:subscriber_worker] = parse_boolean(ENV['MAGELLAN_SUBSCRIBER_WORKER'], true)
30
+ config
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,70 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'magellan/rails'
3
+ require "magellan/worker"
4
+ require "magellan/extentions"
5
+
6
+ require 'json'
7
+ require "bunny"
8
+
9
+ class Magellan::Worker::Core
10
+
11
+ attr_reader :config
12
+
13
+ def initialize()
14
+ # ワーカーの設定ファイルを読み込む
15
+ @config = Magellan::Worker::Config.load_config
16
+ end
17
+
18
+ def initialize!
19
+ # 設定ファイルからRabbitMQへの接続設定を作成
20
+ connection_settings = {
21
+ host: @config[:host],
22
+ port: @config[:port],
23
+ vhost: @config[:vhost],
24
+ user: @config[:rabbitmq_user],
25
+ pass: @config[:rabbitmq_password],
26
+ timeout: 0.3
27
+ }
28
+
29
+ @conn = Bunny.new(connection_settings)
30
+ @conn.start
31
+ @channel = @conn.create_channel
32
+
33
+ queue_name = @config[:request_queue]
34
+ exchange_name = @config[:response_exchange]
35
+
36
+ # no_declareを設定しない場合、キューやエクスチェンジに対してdeclareを実行してしまい、
37
+ # Acess Contorolにひっかかりエラーとなります
38
+ # queueとexchange及びbindはmagellan-conductorによって作成済みの想定です
39
+ @queue = @channel.queue(queue_name, no_declare: true)
40
+ @exchange = @channel.exchange(exchange_name, no_declare: true)
41
+
42
+ @executor = Magellan::Worker::Executor.new(@exchange)
43
+
44
+ self
45
+ end
46
+
47
+ def run
48
+ Magellan.logger.info("====== Magellan Worker start ======")
49
+ @queue.subscribe(block: true, ack: true) do |delivery_info, properties, payload|
50
+ reply_to = properties.reply_to # ワーカー実行結果返却時のルーティングキー
51
+ correlation_id = properties.correlation_id # ワーカー実行結果返却時にどのリクエストに対応するメッセージか判別するための識別子
52
+ delivery_tag = delivery_info.delivery_tag # ackを返却時に使用
53
+
54
+ # ワーカーロジック実行中に落ちたとしてもRabbitMQから再送させないために
55
+ # メッセージを取得した直後にackを返します
56
+ # 第1引数: ackを返す対象のメッセージを指定
57
+ # 第2引数: ackを返す対象以前のメッセージもackを返すか
58
+ # true: 指定したメッセージ以前に受信したメッセージ全てが対象
59
+ # false: 指定したメッセージのみが対象
60
+ @channel.basic_ack(delivery_tag, false)
61
+
62
+ begin
63
+ request_message = JSON.parse(payload)
64
+ @executor.execute(reply_to, correlation_id, delivery_tag, request_message)
65
+ rescue
66
+ Magellan.logger.error("Magellan Worker request execution error: #{$!}\n" + $@.join("\n"))
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,38 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'magellan/rails'
3
+ require "magellan/subscriber"
4
+ require "bunny"
5
+
6
+ class Magellan::Worker::Executor
7
+ def initialize(exchange)
8
+ @exchange = exchange
9
+ @rails_executor = nil
10
+ @subscriber_executor = nil
11
+
12
+ # Rails 環境の初期化は Subscriber だけの時も実施する
13
+ # Rails.root やら Rails.application.config がないと困ることが多いと思うので
14
+ require File.expand_path('config/environment')
15
+
16
+ # production 環境で subscriber として動作する時は eager loading しておく
17
+ if Rails.env.production? and Magellan::Worker.worker.config[:subscriber_worker]
18
+ subscriber_executor
19
+ end
20
+ end
21
+
22
+ def rails_executor
23
+ @rails_executor ||= Magellan::Rails::Executor.new(@exchange)
24
+ end
25
+
26
+ def subscriber_executor
27
+ @subscriber_executor ||= Magellan::Subscriber::Executor.new
28
+ end
29
+
30
+ def execute(reply_to, correlation_id, delivery_tag, request_message)
31
+ case request_message["headers"]["Method"]
32
+ when /\Apublish\z/i
33
+ subscriber_executor.execute(request_message)
34
+ else
35
+ rails_executor.execute(reply_to, correlation_id, delivery_tag, request_message)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,14 @@
1
+ # -*- coding: utf-8 -*-
2
+ require "magellan"
3
+
4
+ module Magellan::Worker
5
+ autoload :Core, 'magellan/worker/core'
6
+ autoload :Executor, 'magellan/worker/executor'
7
+ autoload :Config, 'magellan/worker/config'
8
+
9
+ class << self
10
+ def worker
11
+ @worker ||= Magellan::Worker::Core.new
12
+ end
13
+ end
14
+ end
data/lib/magellan.rb ADDED
@@ -0,0 +1,28 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'logger'
3
+ require 'fileutils'
4
+
5
+ module Magellan
6
+ autoload :Rails, 'magellan/rails'
7
+
8
+ class << self
9
+ attr_accessor :logger
10
+
11
+ # loggerの設定
12
+ # ログレベルは環境変数: MAGELLAN_RAILS_LOG_LEVEL に設定して必要に応じて変更してください
13
+ environment = ENV['RAILS_ENV'] || 'development'
14
+ log_dir = File.expand_path("log")
15
+ FileUtils.makedirs(log_dir) unless File.exists?(log_dir)
16
+ logger = ::Logger.new(File.expand_path("log/#{environment}.log"))
17
+
18
+ log_level_env = ENV['MAGELLAN_RAILS_LOG_LEVEL']
19
+ log_levels = ['debug', 'info', 'warn', 'error', 'fatal', 'unknown']
20
+ if log_level_env && log_levels.include?(log_level_env)
21
+ log_level = Logger.const_get(log_level_env.upcase.to_sym)
22
+ else
23
+ log_level = Logger::INFO
24
+ end
25
+ logger.level = log_level
26
+ Magellan.logger = logger
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'magellan/rails/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "magellan-rails"
8
+ spec.version = Magellan::Rails::VERSION
9
+ spec.authors = ["Yuuki Noguchi"]
10
+ spec.email = ["otonakaoru@gmail.com"]
11
+ spec.summary = %q{}
12
+ spec.description = %q{}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency "railties", '~> 4.1.0'
22
+ spec.add_runtime_dependency "bunny"
23
+ spec.add_runtime_dependency "json"
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.6"
26
+ spec.add_development_dependency "rspec"
27
+ # spec.add_development_dependency "bunny_mock"
28
+ end
@@ -0,0 +1,103 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require "spec_helper"
4
+ require 'json'
5
+
6
+ describe Magellan::Rails::Request do
7
+ describe '#parse_message' do
8
+ before(:all) do
9
+ @request_message_base = {
10
+ 'headers' => {
11
+ 'Title' => 'groovenauts-app',
12
+ 'Interface-Version' => '1.0.0',
13
+ 'Title-Runtime-Version'=> '1.0.0',
14
+ 'Reference-Id' => '192.168.1.110',
15
+ 'Url' => 'http://magellan-clouds.com/worker/test',
16
+ 'Oauth-Requester-Id' => 'tarou',
17
+ 'Content-Type' => 'application/json',
18
+ 'Method' => 'GET',
19
+ "Server-Name" => "localhost",
20
+ "Server-Port" => 3000,
21
+ "Path-Info" => "/hello/index",
22
+ "Query-String" => "qs1=abc&qs2=def"
23
+ },
24
+ 'body' => '',
25
+ 'body_encoding' => 'plain',
26
+ 'options' => {
27
+ # 当分実装される予定がなく、空のハッシュが渡されます
28
+ # 'reply_ttl' => 1000
29
+ }
30
+ }
31
+ end
32
+
33
+ context "valid format" do
34
+ context "body_encoding=nil" do
35
+ let(:request){ Magellan::Rails::Request.new }
36
+
37
+ before do
38
+ @request_message = @request_message_base.dup
39
+ @request_message.delete("body_encoding")
40
+ end
41
+
42
+ subject{ request }
43
+
44
+ it do
45
+ request.parse_message(@request_message)
46
+ expect(request.headers).to eq(@request_message['headers'])
47
+ expect(request.body ).to eq(@request_message['body'])
48
+ expect(request.options).to eq(@request_message['options'])
49
+ end
50
+ end
51
+
52
+ context "body_encoding=plain" do
53
+ let(:request){ Magellan::Rails::Request.new }
54
+
55
+ before do
56
+ @request_message = @request_message_base.dup
57
+ end
58
+
59
+ subject{ request }
60
+
61
+ it do
62
+ request.parse_message(@request_message)
63
+ expect(request.headers).to eq(@request_message['headers'])
64
+ expect(request.body ).to eq(@request_message['body'])
65
+ expect(request.options).to eq(@request_message['options'])
66
+ end
67
+ end
68
+
69
+ context "body_encoding=base64" do
70
+ let(:request){ Magellan::Rails::Request.new }
71
+
72
+ before do
73
+ @request_body = "\xff"
74
+ @request_message = @request_message_base.dup
75
+ @request_message['body'] = Base64.strict_encode64(@request_body)
76
+ @request_message['body_encoding'] = 'base64'
77
+ end
78
+
79
+ subject{ request }
80
+
81
+ it do
82
+ request.parse_message(@request_message)
83
+ expect(request.headers).to eq(@request_message['headers'])
84
+ expect(request.body.inspect).to eq(@request_body.inspect)
85
+ expect(request.options).to eq(@request_message['options'])
86
+ end
87
+ end
88
+ end
89
+
90
+ context "invalid format" do
91
+ it 'raise invalid format exception'
92
+ end
93
+ end
94
+
95
+ describe '#option' do
96
+ end
97
+
98
+ describe '#reply_ttl' do
99
+ end
100
+
101
+ describe '#to_rack_env' do
102
+ end
103
+ end