Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b973e3a
Adding pry to gemfile for easier development
iloveitaly Apr 5, 2016
f476b32
Create translations table
iloveitaly Apr 6, 2016
3f1cddb
Create attempts table
iloveitaly Apr 6, 2016
2c42d0f
Create translation and log response if successful
iloveitaly Apr 6, 2016
c828280
Optionally create attempt in client
iloveitaly Apr 6, 2016
c99c877
Adding translation
iloveitaly Apr 6, 2016
3a2189d
Create attempt
iloveitaly Apr 6, 2016
bdf1215
Test translation creation
iloveitaly Apr 6, 2016
15cbac0
Support more 200 responses. Improved response parsing.
iloveitaly Apr 6, 2016
e4512ed
Adding pry-stack_explorer
iloveitaly Apr 12, 2016
41f0c5c
Adding option to stop reprocessing of jobs
iloveitaly Apr 12, 2016
343b0eb
Ensure configuration state is reset before every test
iloveitaly Apr 12, 2016
dc0e381
o
iloveitaly Apr 13, 2016
cb71eb3
Use postgres instead of sqlite for jsonb support
iloveitaly May 4, 2016
9592cf1
Adding retry method to job
iloveitaly May 6, 2016
a6b8d1e
Fixing standard job usage of log context
iloveitaly May 19, 2016
4a12f34
Adding translation factory
iloveitaly May 25, 2016
2aaf737
Spec for translation object key detection
iloveitaly May 25, 2016
78f34c7
Determine object key and id on the translation level, on save
iloveitaly May 25, 2016
d90eb38
Removing object key and id detection from job
iloveitaly May 25, 2016
51ecc12
Adding faker
iloveitaly May 26, 2016
afd68ba
Generate unique random keys and tokens in connection factory
iloveitaly May 26, 2016
2d97f1d
Make protected conditional for easier unit tests
iloveitaly May 26, 2016
a0d32de
Adding spec for payload_state
iloveitaly May 26, 2016
c6810ae
Adding payload_state to job
iloveitaly May 26, 2016
690cdd3
Adding spec for related translations
iloveitaly May 26, 2016
b1a97be
Make payload_state a public method
iloveitaly Jun 8, 2016
3386d1b
Adding payload_state to to job doubles
iloveitaly Jun 8, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
source 'https://rubygems.org'

gemspec

gem 'pry'
gem 'pry-nav'
gem 'pry-stack_explorer'
53 changes: 50 additions & 3 deletions app/jobs/cangaroo/job.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module Cangaroo
class Job < ActiveJob::Base
include Cangaroo::Log
include Cangaroo::ClassConfiguration

queue_as :cangaroo
Expand All @@ -10,6 +11,8 @@ class Job < ActiveJob::Base
class_configuration :process_response, true

def perform(*)
log.set_context(job: self)

restart_flow(connection_request)
end

Expand All @@ -21,11 +24,32 @@ def transform
{ type.singularize => payload }
end

protected
def payload_state
other_translation = translation.related_translations.first

if other_translation.present?
:updated
else
:new
end
end

protected if !Rails.env.test?

def connection_request
Cangaroo::Webhook::Client.new(destination_connection, path)
.post(transform, @job_id, parameters)
translation.save!

log.info 'attempting translation',
destination_connection: destination_connection.name,
path: path,
translation: translation

response = Cangaroo::Webhook::Client.new(destination_connection, path)
.post(transform, @job_id, parameters, translation)

translation.update_column :response, (response.blank?) ? {} : response

response
end

def restart_flow(response)
Expand All @@ -36,6 +60,11 @@ def restart_flow(response)
return
end

if response.blank?
log.info 'blank response; not processing'
return
end

PerformFlow.call(
source_connection: destination_connection,
json_body: response.to_json,
Expand All @@ -58,5 +87,23 @@ def payload
def destination_connection
@connection ||= Cangaroo::Connection.find_by!(name: connection)
end

def translation
# NOTE @job_id will remain consistent across retries
# TODO we should move this logic to the translation model

@translation ||= Cangaroo::Translation.where(job_id: @job_id).first_or_initialize(
# TODO use job in place of destination connection
# TODO use source job is place of source connection
# ^ this will provide more detail to the user

source_connection: source_connection,
destination_connection: destination_connection,

object_type: self.type,

request: self.payload
)
end
end
end
3 changes: 3 additions & 0 deletions app/models/cangaroo/attempt.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class Cangaroo::Attempt < ActiveRecord::Base
belongs_to :translation
end
63 changes: 63 additions & 0 deletions app/models/cangaroo/translation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
module Cangaroo
class Translation < ActiveRecord::Base
include Cangaroo::Log

belongs_to :destination_connection, class_name: 'Cangaroo::Connection'
belongs_to :source_connection, class_name: 'Cangaroo::Connection'

has_many :attempts

def request=(payload)
super

self.object_id = nil
self.object_key = nil

determine_payload_identifier

self.request
end

def successful?
!!self.response
end

def related_translations
Cangaroo::Translation.where(
object_type: self.object_type,
object_key: self.object_key,
object_id: self.object_id,
)
.where.not(job_id: self.job_id)
end

def determine_object_key_from_payload
Rails.configuration.cangaroo.payload_keys.each do |payload_key|
if self.request[payload_key].present?
return payload_key
end
end

nil
end

def determine_payload_identifier
self.object_key = determine_object_key_from_payload

if self.object_key
self.object_id = self.request[self.object_key]
else
log.info 'unable to find primary key', translation: self
end
end

def retry
Cangaroo::PerformJobs.call(
# TODO this should be abstracted away into a interator that accepts json payloads
json_body: { self.object_type => [ self.request ] }.to_json,
source_connection: self.source_connection,
jobs: Rails.configuration.cangaroo.jobs
)
end
end
end
3 changes: 2 additions & 1 deletion cangaroo.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Gem::Specification.new do |s|
s.add_development_dependency 'rubocop'
s.add_development_dependency 'shoulda-matchers'
s.add_development_dependency 'simplecov'
s.add_development_dependency 'sqlite3'
s.add_development_dependency 'pg'
s.add_development_dependency 'webmock'
s.add_development_dependency 'faker'
end
22 changes: 22 additions & 0 deletions db/migrate/20160405214735_create_translations.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
class CreateTranslations < ActiveRecord::Migration
def change
create_table :cangaroo_translations do |t|
t.references :source_connection, index: true
t.references :destination_connection, index: true

t.string :job_id

t.string :object_type
t.string :object_id
t.string :object_key

t.jsonb :request
t.jsonb :response

t.timestamps null: false
end

add_foreign_key :cangaroo_translations, :cangaroo_connections, column: :source_connection_id
add_foreign_key :cangaroo_translations, :cangaroo_connections, column: :destination_connection_id
end
end
13 changes: 13 additions & 0 deletions db/migrate/20160406003211_create_cangaroo_attempts.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class CreateCangarooAttempts < ActiveRecord::Migration
def change
create_table :cangaroo_attempts do |t|
t.references :translation, index: true
t.integer :response_code
t.jsonb :response

t.timestamps null: false
end

add_foreign_key :cangaroo_attempts, :cangaroo_translations, column: :translation_id
end
end
2 changes: 2 additions & 0 deletions lib/cangaroo/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ class Engine < ::Rails::Engine
Rails.configuration.cangaroo = ActiveSupport::OrderedOptions.new
Rails.configuration.cangaroo.jobs = []
Rails.configuration.cangaroo.poll_jobs = []
Rails.configuration.cangaroo.process_response = true
Rails.configuration.cangaroo.basic_auth = false
Rails.configuration.cangaroo.payload_keys = ['id']
end
end
end
10 changes: 9 additions & 1 deletion lib/cangaroo/webhook/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def initialize(connection, path)
@path = path
end

def post(payload, request_id, parameters)
def post(payload, request_id, parameters, translation = nil)
request_body = body(payload, request_id, parameters).to_json

request_options = {
Expand All @@ -33,6 +33,14 @@ def post(payload, request_id, parameters)

sanitized_response = sanitize_response(req)

if translation.present?
Cangaroo::Attempt.create!(
translation: translation,
response_code: req.response.code,
response: sanitized_response
)
end

if %w(200 201 202 204).include?(req.response.code)
sanitized_response
else
Expand Down
4 changes: 2 additions & 2 deletions spec/factories/cangaroo_connections.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
name :store
url 'www.store.com'
parameters { { first: 'first', second: 'second' } }
key '1e4e888ac66f8dd41e00c5a7ac36a32a9950d271'
token '8d49cddb4291562808bfca1bee8a9f7cf947a987'
key { SecureRandom.hex(13) }
token { SecureRandom.hex(13) }
end
end
17 changes: 17 additions & 0 deletions spec/factories/translation_factory.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FactoryGirl.define do
factory :translation, class: 'Cangaroo::Translation' do
source_connection { FactoryGirl.create(:cangaroo_connection, name: Faker::Company.name, url: Faker::Internet.domain_name) }
destination_connection { FactoryGirl.create(:cangaroo_connection, name: Faker::Company.name, url: Faker::Internet.domain_name) }

job_id { SecureRandom.uuid }
object_type 'customers'

request {
{
"id" => SecureRandom.random_number(1000),
"updated_at" => DateTime.now.iso8601,
"created_at" => DateTime.now.iso8601
}
}
end
end
4 changes: 2 additions & 2 deletions spec/interactors/cangaroo/perform_jobs_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ class JobB < Cangaroo::Job; end
end

describe '.call' do
let(:job_a) { double('job_a', perform?: true, enqueue: nil) }
let(:job_b) { double('job_b', perform?: false, enqueue: nil) }
let(:job_a) { double('job_a', perform?: true, enqueue: nil, payload_state: :new) }
let(:job_b) { double('job_b', perform?: false, enqueue: nil, payload_state: :new) }

context 'payload with objects' do
let(:json_body) { load_fixture('json_payload_ok.json') }
Expand Down
42 changes: 40 additions & 2 deletions spec/jobs/cangaroo/job_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class FakeJob < Cangaroo::Job
let(:job_class) { FakeJob }
let(:destination_connection) { create(:cangaroo_connection) }
let(:type) { 'orders' }
let(:payload) { { id: 'O123' } }
let(:payload) { { "id" => 'O123' } }
let(:connection_response) { parse_fixture('json_payload_connection_response.json') }

let(:options) do
Expand Down Expand Up @@ -43,11 +43,12 @@ class FakeJob < Cangaroo::Job
it 'calls post on client' do
job.perform
expect(client).to have_received(:post)
.with(job.transform, job.job_id, email: 'info@nebulab.it')
.with(job.transform, job.job_id, { email: 'info@nebulab.it' }, an_instance_of(Cangaroo::Translation))
end

it 'restart the flow' do
job.perform

expect(Cangaroo::PerformFlow).to have_received(:call)
.once
.with(source_connection: destination_connection,
Expand All @@ -63,6 +64,31 @@ class FakeJob < Cangaroo::Job
expect(Cangaroo::PerformFlow).to_not have_received(:call)
end

it 'creates a translation record and stores the response' do
job.perform

translation = Cangaroo::Translation.find_by(job_id: job.job_id)

expect(translation).to_not be_nil
expect(translation.successful?).to eq(true)
expect(translation.destination_connection).to eq(destination_connection)
expect(translation.object_type).to eq('orders')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.

expect(translation.object_key).to eq('id')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.

expect(translation.object_id).to eq('O123')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.

end

context 'endpoint communication fails' do
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.

it 'creates a unsuccessful translation model' do
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.

client.stub(:post).and_raise('an error')
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.


expect { job.perform }.to raise_error

translation = Cangaroo::Translation.find_by(job_id: job.job_id)
expect(translation).to_not be_nil
expect(translation.successful?).to eq(false)
end
end

context 'endpoint provides a empty response' do
it 'should not restart the flow' do
allow(client).to receive(:post).and_return('')
Expand All @@ -81,5 +107,17 @@ class FakeJob < Cangaroo::Job
describe '#transform' do
it { expect(job_class.new(options).transform).to eq('order' => payload) }
end

describe '#payload_state' do
let(:job) { job_class.new(options) }

it { expect(job.payload_state).to eq(:new) }

it 'returns updated when a previous matching payload exists' do
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.

allow_any_instance_of(Cangaroo::Translation).to receive(:related_translations).and_return([OpenStruct.new()])

expect(job.payload_state).to eq(:updated)
end
end
end
end
Loading