demux 0.1.0.beta
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/MIT-LICENSE +20 -0
- data/README.md +36 -0
- data/Rakefile +32 -0
- data/app/assets/config/demux_manifest.js +1 -0
- data/app/assets/stylesheets/demux/application.css +15 -0
- data/app/controllers/demux/application_controller.rb +5 -0
- data/app/helpers/demux/application_helper.rb +4 -0
- data/app/jobs/demux/application_job.rb +4 -0
- data/app/mailers/demux/application_mailer.rb +6 -0
- data/app/models/demux/app.rb +66 -0
- data/app/models/demux/application_record.rb +5 -0
- data/app/models/demux/connection.rb +33 -0
- data/app/models/demux/transmission.rb +81 -0
- data/app/views/layouts/demux/application.html.erb +15 -0
- data/config/environment.rb +0 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20200423143645_create_demux_apps.rb +16 -0
- data/db/migrate/20200423144102_create_demux_connections.rb +14 -0
- data/db/migrate/20200505201706_create_demux_transmissions.rb +23 -0
- data/lib/demux.rb +49 -0
- data/lib/demux/demuxer.rb +46 -0
- data/lib/demux/engine.rb +5 -0
- data/lib/demux/signal.rb +55 -0
- data/lib/demux/signal_attributes.rb +44 -0
- data/lib/demux/transmitter.rb +136 -0
- data/lib/demux/version.rb +5 -0
- data/lib/tasks/demux_tasks.rake +4 -0
- metadata +139 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 24735d1c05b065cef6a590c511594ad2e4aee7c51218debb1eb1cfbc859fa50b
|
|
4
|
+
data.tar.gz: 65d933eb7a814ed3da0125d0c313f90b4dc4994ff743c4b8b3b2f434fdc50941
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 922f311246e08d28291199fc61955a710a9d5f5fa6c7fa4f979bfa946f947569933c5bbd152690af011df311d29f156d7138be2db814e31ce9fecd261c1d03d5
|
|
7
|
+
data.tar.gz: e40331c445703f6dfc8753bb8399084ef5f694e92e194761b283bd91d45587df0dcb0e94e80969035cf9f5a50fcab602ac63940518b000e5e46d7c814da53400
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright 2020 Ross Reinhardt
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[](https://travis-ci.com/rreinhardt9/demux)
|
|
2
|
+
|
|
3
|
+
# Demux
|
|
4
|
+
Short description and motivation.
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
How to use my plugin.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
Add this line to your application's Gemfile:
|
|
11
|
+
|
|
12
|
+
```ruby
|
|
13
|
+
gem 'demux'
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
And then execute:
|
|
17
|
+
```bash
|
|
18
|
+
$ bundle
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or install it yourself as:
|
|
22
|
+
```bash
|
|
23
|
+
$ gem install demux
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Contributing
|
|
27
|
+
After cloning repo:
|
|
28
|
+
|
|
29
|
+
- install gems `bundle install`
|
|
30
|
+
- set up the databases `bundle exec rake db:setup`
|
|
31
|
+
- If you run into trouble setting up databases because of a missing postgres role, you can create one by running `psql` and then running `ALTER ROLE postgres LOGIN CREATEDB;`
|
|
32
|
+
- If you cannot start `psql` because you are missing a database named after your local user, you can create one using `createdb`
|
|
33
|
+
- You should not be able to run the tests `bundle exec rake`
|
|
34
|
+
|
|
35
|
+
## License
|
|
36
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
begin
|
|
2
|
+
require 'bundler/setup'
|
|
3
|
+
rescue LoadError
|
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
require 'rdoc/task'
|
|
8
|
+
|
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
11
|
+
rdoc.title = 'Demux'
|
|
12
|
+
rdoc.options << '--line-numbers'
|
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
|
18
|
+
load 'rails/tasks/engine.rake'
|
|
19
|
+
|
|
20
|
+
load 'rails/tasks/statistics.rake'
|
|
21
|
+
|
|
22
|
+
require 'bundler/gem_tasks'
|
|
23
|
+
|
|
24
|
+
require 'rake/testtask'
|
|
25
|
+
|
|
26
|
+
Rake::TestTask.new(:test) do |t|
|
|
27
|
+
t.libs << 'test'
|
|
28
|
+
t.pattern = 'test/**/*_test.rb'
|
|
29
|
+
t.verbose = false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
task default: :test
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
//= link_directory ../stylesheets/demux .css
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
|
3
|
+
* listed below.
|
|
4
|
+
*
|
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
|
7
|
+
*
|
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
|
11
|
+
* It is generally better to create a new file per style scope.
|
|
12
|
+
*
|
|
13
|
+
*= require_tree .
|
|
14
|
+
*= require_self
|
|
15
|
+
*/
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "jwt"
|
|
4
|
+
|
|
5
|
+
module Demux
|
|
6
|
+
# Demux::App represents an external app that can be connected to an account
|
|
7
|
+
# in the parent application.
|
|
8
|
+
class App < ApplicationRecord
|
|
9
|
+
URL_REGEX = %r{\A(http(s?)\://.+)?\z}i.freeze
|
|
10
|
+
|
|
11
|
+
has_many :connections
|
|
12
|
+
has_many :transmissions
|
|
13
|
+
has_secure_token :secret
|
|
14
|
+
|
|
15
|
+
validates :entry_url, :signal_url, format: { with: URL_REGEX }
|
|
16
|
+
|
|
17
|
+
validates :name, presence: true
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def listening_for(signal_name:, account_id:)
|
|
21
|
+
connections = Demux::Connection.listening_for(
|
|
22
|
+
signal_name: signal_name,
|
|
23
|
+
account_id: account_id
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
joins(:connections)
|
|
27
|
+
.merge(connections)
|
|
28
|
+
.where.not(signal_url: nil)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def without_queued_transmissions_for(signal_hash)
|
|
32
|
+
joins(
|
|
33
|
+
<<~SQL
|
|
34
|
+
LEFT OUTER JOIN demux_transmissions
|
|
35
|
+
ON demux_transmissions.app_id = demux_apps.id
|
|
36
|
+
AND demux_transmissions.status = 0
|
|
37
|
+
AND demux_transmissions.uniqueness_hash = '#{signal_hash}'
|
|
38
|
+
SQL
|
|
39
|
+
)
|
|
40
|
+
.where(demux_transmissions: { id: nil })
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def transmission_requested_all(signal_attributes)
|
|
44
|
+
without_queued_transmissions_for(signal_attributes.hashed).each do |app|
|
|
45
|
+
app.transmission_requested(signal_attributes)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def transmission_requested(signal_attributes)
|
|
51
|
+
transmissions.queue(signal_attributes)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Return an entry url with JWT payload for authorization
|
|
55
|
+
#
|
|
56
|
+
# @param data [Hash] data to sign and include in token payload
|
|
57
|
+
# @param exp [Integer] expiration of token in seconds since the epoch
|
|
58
|
+
#
|
|
59
|
+
# @return [String] the entry url with signed token appended
|
|
60
|
+
|
|
61
|
+
def signed_entry_url(data: {}, exp: 1.minute.from_now.to_i)
|
|
62
|
+
token = JWT.encode({ data: data, exp: exp }, secret, "HS256")
|
|
63
|
+
"#{entry_url}?token=#{token}"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Demux
|
|
4
|
+
class Connection < ApplicationRecord
|
|
5
|
+
belongs_to :app
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def listening_for(signal_name:, account_id:)
|
|
9
|
+
where(account_id: account_id)
|
|
10
|
+
.signal(signal_name)
|
|
11
|
+
.or(wildcard_signal)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def signal(signal)
|
|
15
|
+
where("demux_connections.signals @> ?", "{#{signal}}")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def wildcard_signal
|
|
21
|
+
where("demux_connections.signals @> ?", "{*}")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Return an entry url for this specific connection
|
|
26
|
+
#
|
|
27
|
+
# @return [String] the entry url with account_id in signed token
|
|
28
|
+
|
|
29
|
+
def entry_url
|
|
30
|
+
app.signed_entry_url(data: { account_id: account_id })
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Demux
|
|
4
|
+
# Demux::Transmission represents a signal being sent to an app
|
|
5
|
+
class Transmission < ApplicationRecord
|
|
6
|
+
belongs_to :app
|
|
7
|
+
|
|
8
|
+
before_save :update_uniqueness_hash
|
|
9
|
+
|
|
10
|
+
enum status: %i[queued sending success failure]
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def for_app(app_relation)
|
|
14
|
+
joins(:app).merge(app_relation)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def queue(signal_attributes)
|
|
18
|
+
create(signal_attributes.to_hash)
|
|
19
|
+
rescue ActiveRecord::RecordNotUnique
|
|
20
|
+
# Unique index by status/uniqueness_hash
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def transmit
|
|
25
|
+
return self unless attributes_required_to_transmit_present?
|
|
26
|
+
|
|
27
|
+
update(
|
|
28
|
+
request_url: app.signal_url,
|
|
29
|
+
request_body: payload.to_json,
|
|
30
|
+
status: :sending
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
save_receipt(Transmitter.new(self).transmit.receipt)
|
|
34
|
+
|
|
35
|
+
self
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def save_receipt(receipt)
|
|
39
|
+
update(
|
|
40
|
+
status: receipt.success? ? :success : :failure,
|
|
41
|
+
response_code: receipt.http_code,
|
|
42
|
+
response_body: receipt.response_body,
|
|
43
|
+
request_headers: receipt.request_headers
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def signal_name
|
|
48
|
+
signal.signal_name
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def signature
|
|
52
|
+
OpenSSL::HMAC.hexdigest("SHA256", app.secret, request_body)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def signal_url
|
|
58
|
+
app.signal_url
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def payload
|
|
62
|
+
@payload ||= { action: action }.merge(signal.payload_for(action))
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def signal
|
|
66
|
+
@signal ||= signal_class.constantize.new(
|
|
67
|
+
object_id, account_id: account_id
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def update_uniqueness_hash
|
|
72
|
+
return unless attributes_required_to_transmit_present?
|
|
73
|
+
|
|
74
|
+
self.uniqueness_hash = SignalAttributes.from_object(self).hashed
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def attributes_required_to_transmit_present?
|
|
78
|
+
account_id? && action? && object_id? && signal_class?
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
File without changes
|
data/config/routes.rb
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class CreateDemuxApps < ActiveRecord::Migration[5.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :demux_apps do |t|
|
|
4
|
+
t.string :name
|
|
5
|
+
t.text :description
|
|
6
|
+
t.string :secret
|
|
7
|
+
t.string :entry_url
|
|
8
|
+
t.string :signal_url
|
|
9
|
+
t.text :signals, array:true, default: []
|
|
10
|
+
|
|
11
|
+
t.timestamps
|
|
12
|
+
end
|
|
13
|
+
add_index :demux_apps, :signals, using: "gin"
|
|
14
|
+
add_index :demux_apps, :secret, unique: true
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class CreateDemuxConnections < ActiveRecord::Migration[5.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :demux_connections do |t|
|
|
4
|
+
t.integer :account_id
|
|
5
|
+
t.integer :app_id
|
|
6
|
+
t.text :signals, array:true, default: []
|
|
7
|
+
|
|
8
|
+
t.timestamps
|
|
9
|
+
end
|
|
10
|
+
add_index :demux_connections, :signals, using: "gin"
|
|
11
|
+
add_index :demux_connections, :account_id
|
|
12
|
+
add_index :demux_connections, :app_id
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class CreateDemuxTransmissions < ActiveRecord::Migration[5.2]
|
|
2
|
+
def change
|
|
3
|
+
create_table :demux_transmissions do |t|
|
|
4
|
+
t.string :signal_class
|
|
5
|
+
t.string :action
|
|
6
|
+
t.integer :object_id
|
|
7
|
+
t.integer :app_id
|
|
8
|
+
t.integer :account_id
|
|
9
|
+
t.integer :status, default: 0
|
|
10
|
+
t.string :response_code
|
|
11
|
+
t.jsonb :response_headers
|
|
12
|
+
t.text :response_body
|
|
13
|
+
t.jsonb :request_headers
|
|
14
|
+
t.text :request_body
|
|
15
|
+
t.string :request_url
|
|
16
|
+
t.string :uniqueness_hash
|
|
17
|
+
|
|
18
|
+
t.timestamps
|
|
19
|
+
end
|
|
20
|
+
add_index :demux_transmissions, :app_id
|
|
21
|
+
add_index :demux_transmissions, [:uniqueness_hash, :app_id], unique: true, where: "status = 0"
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/demux.rb
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "demux/engine"
|
|
4
|
+
require "demux/demuxer"
|
|
5
|
+
require "demux/signal"
|
|
6
|
+
require "demux/signal_attributes"
|
|
7
|
+
require "demux/transmitter"
|
|
8
|
+
|
|
9
|
+
# Demux toplevel namespace
|
|
10
|
+
module Demux
|
|
11
|
+
# Access the current configuration
|
|
12
|
+
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def configuration
|
|
16
|
+
@configuration ||= Configuration.new
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Alias so that we can refer to configuration as config
|
|
20
|
+
|
|
21
|
+
def config
|
|
22
|
+
configuration
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Configure the library
|
|
26
|
+
#
|
|
27
|
+
# @yieldparam [Demux::Configuration] current_configuration
|
|
28
|
+
#
|
|
29
|
+
# @example
|
|
30
|
+
# Demux.configure do |config|
|
|
31
|
+
# config.default_demuxer = "Demux::Demuxer"
|
|
32
|
+
# end
|
|
33
|
+
#
|
|
34
|
+
# @yieldreturn [Demux::Configuration]
|
|
35
|
+
|
|
36
|
+
def configure
|
|
37
|
+
yield configuration
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Configuration holds the current configuration for the SeisimicAPI
|
|
41
|
+
# and provides defaults
|
|
42
|
+
class Configuration
|
|
43
|
+
attr_accessor :default_demuxer
|
|
44
|
+
|
|
45
|
+
def initialize(args = {})
|
|
46
|
+
@default_demuxer = args.fetch(:default_demuxer) { Demux::Demuxer }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Demux
|
|
4
|
+
# Demux::Demuxer is the heart of pairing signals to apps.
|
|
5
|
+
# It's a base implementation of what needs to happen, but apps can
|
|
6
|
+
# provide their own custom demuxer that calls out to this one.
|
|
7
|
+
#
|
|
8
|
+
# For example a host application will likely want to process signals in a
|
|
9
|
+
# background queue and can supply their own demuxer with details on how that
|
|
10
|
+
# should happen.
|
|
11
|
+
class Demuxer
|
|
12
|
+
def initialize(signal_attributes)
|
|
13
|
+
@signal_attributes = signal_attributes
|
|
14
|
+
@account_id = @signal_attributes.account_id
|
|
15
|
+
@signal_class = @signal_attributes.signal_class
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def send_to_apps
|
|
19
|
+
queue_transmissions
|
|
20
|
+
|
|
21
|
+
queued_transmissions.each(&:transmit)
|
|
22
|
+
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def queued_transmissions
|
|
27
|
+
Transmission
|
|
28
|
+
.queued
|
|
29
|
+
.for_app(listening_apps)
|
|
30
|
+
.where(uniqueness_hash: @signal_attributes.hashed)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def listening_apps
|
|
34
|
+
Demux::App.listening_for(
|
|
35
|
+
signal_name: @signal_class.constantize.signal_name,
|
|
36
|
+
account_id: @account_id
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def queue_transmissions
|
|
41
|
+
listening_apps.transmission_requested_all(@signal_attributes)
|
|
42
|
+
|
|
43
|
+
self
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/demux/engine.rb
ADDED
data/lib/demux/signal.rb
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Demux
|
|
4
|
+
# All signals will inherit from Demux::Signal. A signal represent a message
|
|
5
|
+
# to be demuxed and sent to apps.
|
|
6
|
+
class Signal
|
|
7
|
+
attr_reader :account_id
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
attr_reader :object_class, :signal_name
|
|
11
|
+
|
|
12
|
+
def attributes(attr)
|
|
13
|
+
@object_class = attr.fetch(:object_class)
|
|
14
|
+
@signal_name = attr.fetch(:signal_name)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def signal_name
|
|
19
|
+
self.class.signal_name
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(object_id,
|
|
23
|
+
account_id:,
|
|
24
|
+
demuxer: Demux.config.default_demuxer)
|
|
25
|
+
@object_id = Integer(object_id)
|
|
26
|
+
@account_id = account_id
|
|
27
|
+
@demuxer = demuxer
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def object
|
|
31
|
+
@object ||= self.class.object_class.find(@object_id)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def payload_for(action)
|
|
35
|
+
if respond_to?("#{action}_payload")
|
|
36
|
+
public_send("#{action}_payload")
|
|
37
|
+
else
|
|
38
|
+
payload
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def send(action)
|
|
43
|
+
@demuxer.new(
|
|
44
|
+
SignalAttributes.new(
|
|
45
|
+
account_id: @account_id,
|
|
46
|
+
action: String(action),
|
|
47
|
+
object_id: @object_id,
|
|
48
|
+
signal_class: self.class.name
|
|
49
|
+
)
|
|
50
|
+
).send_to_apps
|
|
51
|
+
|
|
52
|
+
self
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Demux
|
|
4
|
+
# Attributes that are commonly used to identify a signal
|
|
5
|
+
class SignalAttributes
|
|
6
|
+
attr_reader :account_id, :action, :object_id, :signal_class
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def from_object(object)
|
|
10
|
+
new(
|
|
11
|
+
account_id: object.account_id,
|
|
12
|
+
action: object.action,
|
|
13
|
+
object_id: object.object_id,
|
|
14
|
+
signal_class: object.signal_class
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(account_id:, action:, object_id:, signal_class:)
|
|
20
|
+
@account_id = account_id
|
|
21
|
+
@action = action
|
|
22
|
+
@object_id = object_id
|
|
23
|
+
@signal_class = String(signal_class)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_hash
|
|
27
|
+
{
|
|
28
|
+
account_id: @account_id,
|
|
29
|
+
action: @action,
|
|
30
|
+
object_id: @object_id,
|
|
31
|
+
signal_class: @signal_class
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def hashed
|
|
36
|
+
Base64.strict_encode64({
|
|
37
|
+
account_id: account_id,
|
|
38
|
+
action: action,
|
|
39
|
+
object_id: object_id,
|
|
40
|
+
signal_class: signal_class
|
|
41
|
+
}.to_json)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
|
|
5
|
+
module Demux
|
|
6
|
+
# Transmit a Transmission
|
|
7
|
+
class Transmitter
|
|
8
|
+
attr_reader :receipt
|
|
9
|
+
# Constructor
|
|
10
|
+
#
|
|
11
|
+
# @param transmission [Demux::Transmission] the transmission to be sent
|
|
12
|
+
#
|
|
13
|
+
# @return [self] the initialized Demux::Transmitter
|
|
14
|
+
|
|
15
|
+
def initialize(transmission)
|
|
16
|
+
@transmission = transmission
|
|
17
|
+
@uri = URI(@transmission.request_url)
|
|
18
|
+
@receipt = NullTransmissionReceipt.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Use the transmitter to send it's transmission
|
|
22
|
+
#
|
|
23
|
+
# @return [self]
|
|
24
|
+
|
|
25
|
+
def transmit
|
|
26
|
+
build_request
|
|
27
|
+
|
|
28
|
+
send_request
|
|
29
|
+
|
|
30
|
+
@receipt = TransmissionReceipt.new(@request, @response)
|
|
31
|
+
|
|
32
|
+
log_transmission
|
|
33
|
+
|
|
34
|
+
self
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def build_request
|
|
40
|
+
@request = Net::HTTP::Post.new(@uri).tap do |request|
|
|
41
|
+
request["X-Demux-Signal"] = @transmission.signal_name
|
|
42
|
+
request["X-Demux-Signature"] = @transmission.signature
|
|
43
|
+
request["Content-Type"] = "application/json"
|
|
44
|
+
request["User-Agent"] = "Demux"
|
|
45
|
+
request.body = @transmission.request_body
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def send_request
|
|
50
|
+
@response =
|
|
51
|
+
Net::HTTP.start(@uri.hostname, @uri.port, use_ssl: true) do |http|
|
|
52
|
+
http.request(@request)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def log_transmission
|
|
57
|
+
Rails.logger.debug(
|
|
58
|
+
"Send #{@transmission.signal_name}/#{@transmission.action} signal \
|
|
59
|
+
to #{@uri} \
|
|
60
|
+
with payload #{@transmission.request_body}"
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Null object to represent having no receipt
|
|
66
|
+
class NullTransmissionReceipt
|
|
67
|
+
def success?
|
|
68
|
+
nil
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def http_code
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def request_headers
|
|
76
|
+
{}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def request_body
|
|
80
|
+
""
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def response_body
|
|
84
|
+
""
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Returned when the Transmitter transmits
|
|
89
|
+
# @see Demux::Transmitter
|
|
90
|
+
class TransmissionReceipt
|
|
91
|
+
def initialize(request, response)
|
|
92
|
+
@raw_request = request
|
|
93
|
+
@raw_response = response
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Was the response code 2xx
|
|
97
|
+
#
|
|
98
|
+
# @return [Boolean]
|
|
99
|
+
|
|
100
|
+
def success?
|
|
101
|
+
@raw_response.is_a?(Net::HTTPSuccess)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# HTTP code of response
|
|
105
|
+
#
|
|
106
|
+
# @return [Integer] http code
|
|
107
|
+
|
|
108
|
+
def http_code
|
|
109
|
+
Integer(@raw_response.code)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Headers that were sent with request
|
|
113
|
+
#
|
|
114
|
+
# @return [Hash] Hash of headers
|
|
115
|
+
|
|
116
|
+
def request_headers
|
|
117
|
+
@raw_request.each_header.to_h
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Body of the request
|
|
121
|
+
#
|
|
122
|
+
# @return [String] request body
|
|
123
|
+
|
|
124
|
+
def request_body
|
|
125
|
+
@raw_request.body
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Body of the response
|
|
129
|
+
#
|
|
130
|
+
# @return [String] response body
|
|
131
|
+
|
|
132
|
+
def response_body
|
|
133
|
+
@raw_response.body
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: demux
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0.beta
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Ross Reinhardt
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2020-05-29 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: jwt
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.5'
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '3'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: '1.5'
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3'
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: rails
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '5.1'
|
|
40
|
+
- - "<"
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '7'
|
|
43
|
+
type: :runtime
|
|
44
|
+
prerelease: false
|
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '5.1'
|
|
50
|
+
- - "<"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '7'
|
|
53
|
+
- !ruby/object:Gem::Dependency
|
|
54
|
+
name: pg
|
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '0'
|
|
60
|
+
type: :development
|
|
61
|
+
prerelease: false
|
|
62
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '0'
|
|
67
|
+
- !ruby/object:Gem::Dependency
|
|
68
|
+
name: webmock
|
|
69
|
+
requirement: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '0'
|
|
74
|
+
type: :development
|
|
75
|
+
prerelease: false
|
|
76
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '0'
|
|
81
|
+
description: Configure your application to send signals to a constellation of other
|
|
82
|
+
'apps'
|
|
83
|
+
email:
|
|
84
|
+
- rreinhardt9@gmail.com
|
|
85
|
+
executables: []
|
|
86
|
+
extensions: []
|
|
87
|
+
extra_rdoc_files: []
|
|
88
|
+
files:
|
|
89
|
+
- MIT-LICENSE
|
|
90
|
+
- README.md
|
|
91
|
+
- Rakefile
|
|
92
|
+
- app/assets/config/demux_manifest.js
|
|
93
|
+
- app/assets/stylesheets/demux/application.css
|
|
94
|
+
- app/controllers/demux/application_controller.rb
|
|
95
|
+
- app/helpers/demux/application_helper.rb
|
|
96
|
+
- app/jobs/demux/application_job.rb
|
|
97
|
+
- app/mailers/demux/application_mailer.rb
|
|
98
|
+
- app/models/demux/app.rb
|
|
99
|
+
- app/models/demux/application_record.rb
|
|
100
|
+
- app/models/demux/connection.rb
|
|
101
|
+
- app/models/demux/transmission.rb
|
|
102
|
+
- app/views/layouts/demux/application.html.erb
|
|
103
|
+
- config/environment.rb
|
|
104
|
+
- config/routes.rb
|
|
105
|
+
- db/migrate/20200423143645_create_demux_apps.rb
|
|
106
|
+
- db/migrate/20200423144102_create_demux_connections.rb
|
|
107
|
+
- db/migrate/20200505201706_create_demux_transmissions.rb
|
|
108
|
+
- lib/demux.rb
|
|
109
|
+
- lib/demux/demuxer.rb
|
|
110
|
+
- lib/demux/engine.rb
|
|
111
|
+
- lib/demux/signal.rb
|
|
112
|
+
- lib/demux/signal_attributes.rb
|
|
113
|
+
- lib/demux/transmitter.rb
|
|
114
|
+
- lib/demux/version.rb
|
|
115
|
+
- lib/tasks/demux_tasks.rake
|
|
116
|
+
homepage: https://github.com/rreinhardt9/demux
|
|
117
|
+
licenses:
|
|
118
|
+
- MIT
|
|
119
|
+
metadata: {}
|
|
120
|
+
post_install_message:
|
|
121
|
+
rdoc_options: []
|
|
122
|
+
require_paths:
|
|
123
|
+
- lib
|
|
124
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
125
|
+
requirements:
|
|
126
|
+
- - ">="
|
|
127
|
+
- !ruby/object:Gem::Version
|
|
128
|
+
version: '0'
|
|
129
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
130
|
+
requirements:
|
|
131
|
+
- - ">"
|
|
132
|
+
- !ruby/object:Gem::Version
|
|
133
|
+
version: 1.3.1
|
|
134
|
+
requirements: []
|
|
135
|
+
rubygems_version: 3.0.3
|
|
136
|
+
signing_key:
|
|
137
|
+
specification_version: 4
|
|
138
|
+
summary: Configure your application to send signals to a constellation of other 'apps'
|
|
139
|
+
test_files: []
|