external_services 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +15 -0
- data/.gitignore +13 -0
- data/.overcommit.yml +32 -0
- data/.rspec +2 -0
- data/.rubocop.yml +32 -0
- data/.travis.yml +19 -0
- data/Gemfile +17 -0
- data/LICENSE.txt +21 -0
- data/README.md +35 -0
- data/Rakefile +28 -0
- data/app/jobs/external_services/api_job.rb +16 -0
- data/app/models/external_services/api_action.rb +86 -0
- data/app/models/external_services/service.rb +40 -0
- data/bin/rails +13 -0
- data/external_services.gemspec +23 -0
- data/lib/external_services/active_record.rb +256 -0
- data/lib/external_services/api.rb +78 -0
- data/lib/external_services/engine.rb +11 -0
- data/lib/external_services/version.rb +3 -0
- data/lib/external_services.rb +14 -0
- data/lib/generators/install_generator.rb +47 -0
- data/lib/generators/service_generator.rb +43 -0
- data/lib/generators/templates/migrations/create_external_services.rb +12 -0
- data/lib/generators/templates/migrations/create_external_services_api_actions.rb +20 -0
- data/lib/generators/templates/services/api.rb +15 -0
- data/lib/generators/templates/services/api_action.rb +9 -0
- data/lib/generators/templates/services/api_job.rb +4 -0
- data/lib/generators/templates/services/model.rb +9 -0
- metadata +106 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
YTA5YzliMjZkZmUyYTY3ZTMzNzEwYTA0NGUxNWEwYjgyYTc4ZmFkOA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MDk4MGE0ZWFjMTYzNzMyZDFiOTEyNTFjMmY5OGVlYmRlYmJiMjQwOA==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
ZTJmNmZmMjY1NTQ3MWNjNjIwYjM2ZWQyNmRkNDdkNDcwMjcxOGRhMTM2Yjc2
|
10
|
+
YThhODk2OGYyODZkZDlmZmM2MzRiMGNmNGQ0YmZjZDAxZjljNWE0ZGU2YmM3
|
11
|
+
ZGYzNDFjYmUyOWZkMTAxNzg1MjBmYTdiMzQxZjY5MjgzNzUxMzc=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
N2Q5NjJlZjAyMmFmM2RhOWNjZTIyYTRkOGVhOWQ5ODZiZGMyMDE3MTJiZWUy
|
14
|
+
YThiZDMwMjQwZmQ3YzU0OThmMTRmY2VkY2Y1ZmZiNDQyMmM4NTNiNGMwZjVm
|
15
|
+
OGU1YWFmMGU5MWMyZmEwNjhjNTg3ODI4NjZlNDYyZTkyOTEyMzc=
|
data/.gitignore
ADDED
data/.overcommit.yml
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# Use this file to configure the Overcommit hooks you wish to use. This will
|
2
|
+
# extend the default configuration defined in:
|
3
|
+
# https://github.com/brigade/overcommit/blob/master/config/default.yml
|
4
|
+
#
|
5
|
+
# At the topmost level of this YAML file is a key representing type of hook
|
6
|
+
# being run (e.g. pre-commit, commit-msg, etc.). Within each type you can
|
7
|
+
# customize each hook, such as whether to only run it on certain files (via
|
8
|
+
# `include`), whether to only display output if it fails (via `quiet`), etc.
|
9
|
+
#
|
10
|
+
# For a complete list of hooks, see:
|
11
|
+
# https://github.com/brigade/overcommit/tree/master/lib/overcommit/hook
|
12
|
+
#
|
13
|
+
# For a complete list of options that you can use to customize hooks, see:
|
14
|
+
# https://github.com/brigade/overcommit#configuration
|
15
|
+
#
|
16
|
+
# Uncomment the following lines to make the configuration take effect.
|
17
|
+
|
18
|
+
#PreCommit:
|
19
|
+
# RuboCop:
|
20
|
+
# enabled: true
|
21
|
+
# on_warn: fail # Treat all warnings as failures
|
22
|
+
#
|
23
|
+
# TrailingWhitespace:
|
24
|
+
# exclude:
|
25
|
+
# - '**/db/structure.sql' # Ignore trailing whitespace in generated files
|
26
|
+
#
|
27
|
+
#PostCheckout:
|
28
|
+
# ALL: # Special hook name that customizes all hooks of this type
|
29
|
+
# quiet: true # Change all post-checkout hooks to only display output on failure
|
30
|
+
#
|
31
|
+
# IndexTags:
|
32
|
+
# enabled: true # Generate a tags file with `ctags` each time HEAD changes
|
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 2.2
|
3
|
+
Exclude:
|
4
|
+
- 'lib/generators/templates/**/*'
|
5
|
+
|
6
|
+
Metrics/LineLength:
|
7
|
+
Max: 140
|
8
|
+
|
9
|
+
Metrics/AbcSize:
|
10
|
+
Max: 25
|
11
|
+
|
12
|
+
Metrics/MethodLength:
|
13
|
+
Max: 20
|
14
|
+
|
15
|
+
Metrics/ModuleLength:
|
16
|
+
Max: 200
|
17
|
+
|
18
|
+
Metrics/ClassLength:
|
19
|
+
Max: 300
|
20
|
+
|
21
|
+
Metrics/CyclomaticComplexity:
|
22
|
+
Max: 8
|
23
|
+
|
24
|
+
Metrics/PerceivedComplexity:
|
25
|
+
Max: 8
|
26
|
+
|
27
|
+
|
28
|
+
Style/Documentation:
|
29
|
+
Enabled: false
|
30
|
+
|
31
|
+
Style/PredicateName:
|
32
|
+
Enabled: false
|
data/.travis.yml
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
sudo: false
|
2
|
+
language: ruby
|
3
|
+
rvm:
|
4
|
+
- 2.2.4
|
5
|
+
before_install: gem install bundler -v 1.12.5
|
6
|
+
env:
|
7
|
+
- RAILS_ENV=test
|
8
|
+
script:
|
9
|
+
- bundle exec rake db:create --trace
|
10
|
+
- bundle exec rake db:migrate --trace
|
11
|
+
- bundle exec rake
|
12
|
+
deploy:
|
13
|
+
provider: rubygems
|
14
|
+
api_key:
|
15
|
+
secure: rgni1lDjNhUTL8NUdJA4oSYu+vzB1Fp8TJ5wVhJfW8E0NUD5YItodmc52RLv3Hwt2501Dqx4pt4WaIP3E71OAG3KzsPUY1NPoCqP4KpXo6c7T1qqFieKztDa8aGOCqjkQ/1to9yt6qdKg6PtjsSWtKu8f3ROhLpBmd4r2phnb1GucKRnH5yaQQtcoNb8AmnE0VBEF2wzALsMg2QdZRk/1qJInbgUldc10FG/33DW71h5IKR8/n2L0Ggm+/ZYe2Rg0aGUE3VHHSkHJFvoHIjwfYaNHu7FrUjYywM4XfZBdl+bdL3a6Op8p+2VRAPMb4Wdb6K0O+f5vGQyvntvR3pMuqFVLC0mL9mkz8RYK7VXVxBfxZk3LQvA15oN52uvN523iKTjM+VvzmEIhZ1+oozN5W9XxT6VSqZTo7rj4lQto0g6+9Ev4kVP92JGMT0GlJcPcZ8PlW7j9phvEXgy89IvLLvF63nVlWw7xAY4pZUlTd185sfSWWPFSyqX4W/djqHq963MB0H8k6lGNnwLf+xFCMScaM52hZ+TEvtapX7nCn7dzgWv8s6kwYqWPa9J0Tegxr364RL7pq0mXEf7JwNivAYv7aAR8PHAg7y15Qw/MXRzoUWG5G5AfZii+GY6cp0Cyo2am0ut05OudYcuohifP4DWkn0wlwDT/QYLv8bMhaE=
|
16
|
+
gem: external_services
|
17
|
+
on:
|
18
|
+
tags: true
|
19
|
+
repo: flant/external_services
|
data/Gemfile
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
source 'https://rubygems.org'
|
2
|
+
|
3
|
+
# Specify your gem's dependencies in external_services.gemspec
|
4
|
+
gemspec
|
5
|
+
|
6
|
+
gem 'sqlite3'
|
7
|
+
gem 'bundler', '~> 1.12'
|
8
|
+
gem 'rake', '~> 10.0'
|
9
|
+
gem 'travis', '~> 1.8', '>= 1.8.2'
|
10
|
+
|
11
|
+
gem 'rubocop', '~> 0.42', require: false
|
12
|
+
gem 'overcommit'
|
13
|
+
|
14
|
+
gem 'pry-rails'
|
15
|
+
|
16
|
+
gem 'factory_girl_rails'
|
17
|
+
gem 'rspec-rails'
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Sergey Gnuskov
|
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
|
13
|
+
all 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
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# ExternalServices [![Build Status](https://travis-ci.org/flant/external_services.svg?branch=master)](https://travis-ci.org/flant/external_services)
|
2
|
+
|
3
|
+
Gem helps syncronizing objects to different external services like Gitlab, Redmine and any other.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'external_services'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install external_services
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
TODO: Write usage instructions here
|
24
|
+
|
25
|
+
## Development
|
26
|
+
|
27
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
28
|
+
|
29
|
+
## Contributing
|
30
|
+
|
31
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/flant/external_services.
|
32
|
+
|
33
|
+
## License
|
34
|
+
|
35
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,28 @@
|
|
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 = 'Asdf'
|
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("../spec/dummy/Rakefile", __FILE__)
|
18
|
+
|
19
|
+
load 'rails/tasks/engine.rake'
|
20
|
+
|
21
|
+
load 'rails/tasks/statistics.rake'
|
22
|
+
|
23
|
+
require 'bundler/gem_tasks'
|
24
|
+
require 'rspec/core/rake_task'
|
25
|
+
|
26
|
+
RSpec::Core::RakeTask.new(:spec)
|
27
|
+
|
28
|
+
task default: :spec
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ExternalServices
|
2
|
+
class ApiJob < ActiveJob::Base
|
3
|
+
queue_as :default
|
4
|
+
|
5
|
+
def action_class
|
6
|
+
"ExternalServices::ApiActions::#{self.class.to_s.demodulize.gsub(/ApiJob/, '')}".constantize
|
7
|
+
end
|
8
|
+
|
9
|
+
def perform(action_id)
|
10
|
+
action = action_class.find(action_id)
|
11
|
+
return if action.processed?
|
12
|
+
|
13
|
+
action.execute!
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module ExternalServices
|
2
|
+
class ApiAction < ::ActiveRecord::Base
|
3
|
+
self.table_name = :external_services_api_actions
|
4
|
+
|
5
|
+
belongs_to :initiator, polymorphic: true
|
6
|
+
|
7
|
+
validates :initiator_id, :initiator_type, :method, :path, :queue, presence: true
|
8
|
+
validate :path_format_correctness
|
9
|
+
|
10
|
+
serialize :data, JSON
|
11
|
+
serialize :options, JSON
|
12
|
+
|
13
|
+
scope :unprocessed, -> { where(processed_at: nil) }
|
14
|
+
|
15
|
+
before_validation :assign_queue
|
16
|
+
before_validation :process_data
|
17
|
+
after_commit :kick_active_job
|
18
|
+
|
19
|
+
before_create :calculate_signature
|
20
|
+
|
21
|
+
def processed?
|
22
|
+
processed_at.present?
|
23
|
+
end
|
24
|
+
|
25
|
+
def set_processed!
|
26
|
+
update_attributes! processed_at: Time.now
|
27
|
+
end
|
28
|
+
|
29
|
+
def initiator_class
|
30
|
+
# Need to use initiator object for STI in polymorphic.. But still will be bugs with deleted STI object
|
31
|
+
initiator.try(:class) || initiator_type.constantize
|
32
|
+
end
|
33
|
+
|
34
|
+
def api_disabled?
|
35
|
+
initiator_class.send(:"#{self.class.to_s.demodulize.underscore}_api_disabled")
|
36
|
+
end
|
37
|
+
|
38
|
+
def change_external_id?
|
39
|
+
options['change_external_id']
|
40
|
+
end
|
41
|
+
|
42
|
+
def job_class
|
43
|
+
"ExternalServices::#{self.class.to_s.demodulize}ApiJob".constantize
|
44
|
+
end
|
45
|
+
|
46
|
+
def kick_active_job
|
47
|
+
return if api_disabled?
|
48
|
+
|
49
|
+
job_class.set(queue: queue).perform_later(id)
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.perform_unprocessed
|
53
|
+
Rails.logger.info "Running unprocessed #{self.class.name.demodulize} api actions..."
|
54
|
+
|
55
|
+
unprocessed.each(&:kick_active_job)
|
56
|
+
end
|
57
|
+
|
58
|
+
def execute!
|
59
|
+
raise NotImplementedError
|
60
|
+
end
|
61
|
+
|
62
|
+
protected
|
63
|
+
|
64
|
+
def assign_queue
|
65
|
+
self.queue ||= case method.to_sym
|
66
|
+
when :create
|
67
|
+
:create
|
68
|
+
when :destroy
|
69
|
+
:delete
|
70
|
+
else
|
71
|
+
:default
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def process_data
|
76
|
+
self.data = nil if method.to_sym == :delete # DELETE has no body
|
77
|
+
end
|
78
|
+
|
79
|
+
def calculate_signature
|
80
|
+
end
|
81
|
+
|
82
|
+
def path_format_correctness
|
83
|
+
errors.add(:path, :invalid) if path =~ %r{//}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module ExternalServices
|
2
|
+
class Service < ::ActiveRecord::Base
|
3
|
+
self.table_name = :external_services
|
4
|
+
|
5
|
+
belongs_to :subject, polymorphic: true
|
6
|
+
serialize :extra_data, JSON
|
7
|
+
|
8
|
+
after_update :on_first_sync, if: -> { external_id_was.nil? && external_id_changed? }
|
9
|
+
|
10
|
+
def self.to_sym
|
11
|
+
to_s.demodulize.underscore.to_sym
|
12
|
+
end
|
13
|
+
|
14
|
+
def on_subject_create(subj)
|
15
|
+
method = subj.send("#{api_name}_id").present? ? :put : :post
|
16
|
+
subj.send("#{api_name}_api_action", method)
|
17
|
+
end
|
18
|
+
|
19
|
+
def on_subject_update(subj)
|
20
|
+
method = subj.send("#{api_name}_id").present? ? :put : :post
|
21
|
+
return true if (subj.respond_to?(:became_archived?) && subj.became_archived?) && method == :post
|
22
|
+
subj.send("#{api_name}_api_action", method)
|
23
|
+
end
|
24
|
+
|
25
|
+
def on_subject_destroy(subj)
|
26
|
+
subj.send("#{api_name}_api_action", :delete)
|
27
|
+
end
|
28
|
+
|
29
|
+
def api_name
|
30
|
+
self.class.to_sym
|
31
|
+
end
|
32
|
+
|
33
|
+
protected
|
34
|
+
|
35
|
+
def on_first_sync
|
36
|
+
callback_name = "on_#{api_name}_first_sync"
|
37
|
+
subject.send(callback_name) if subject.respond_to?(callback_name)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/bin/rails
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# This command will automatically be run when you run "rails" with Rails gems
|
3
|
+
# installed from the root of your application.
|
4
|
+
|
5
|
+
ENGINE_ROOT = File.expand_path('../..', __FILE__)
|
6
|
+
ENGINE_PATH = File.expand_path('../../lib/external_services/engine', __FILE__)
|
7
|
+
|
8
|
+
# Set up gems listed in the Gemfile.
|
9
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
|
10
|
+
require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
|
11
|
+
|
12
|
+
require 'rails/all'
|
13
|
+
require 'rails/engine/commands'
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'external_services/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'external_services'
|
8
|
+
spec.version = ExternalServices::VERSION
|
9
|
+
spec.authors = ['Sergey Gnuskov']
|
10
|
+
spec.email = ['sergey.gnuskov@flant.com']
|
11
|
+
|
12
|
+
spec.summary = 'Gem helps syncronizing objects to different external services like Gitlab, Redmine and any other.'
|
13
|
+
spec.homepage = 'https://github.com/flant/external_services'
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = 'exe'
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_dependency 'faraday', '>= 0.9'
|
22
|
+
spec.add_dependency 'rails', ['>= 4.2.5', '< 6.0']
|
23
|
+
end
|
@@ -0,0 +1,256 @@
|
|
1
|
+
module ExternalServices
|
2
|
+
module ActiveRecord
|
3
|
+
module HasExternalService
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def has_external_service(name, options = {})
|
8
|
+
class_attribute :external_services unless respond_to?(:external_services)
|
9
|
+
self.external_services ||= {}
|
10
|
+
self.external_services[name.to_sym] = options
|
11
|
+
|
12
|
+
unless options[:only_api_actions] == true
|
13
|
+
service_class = get_service_class(name, options)
|
14
|
+
service_assoc = :"#{name}_service"
|
15
|
+
has_one service_assoc, class_name: service_class, as: :subject, dependent: :destroy, autosave: true
|
16
|
+
|
17
|
+
define_external_service_getset name, options
|
18
|
+
define_external_service_sync_methods name, options
|
19
|
+
end
|
20
|
+
|
21
|
+
define_external_service_callbacks name, options
|
22
|
+
define_external_service_helper_methods name, options
|
23
|
+
|
24
|
+
extend service_class::SubjectClassMethods if defined? service_class::SubjectClassMethods
|
25
|
+
include service_class::SubjectMethods if defined? service_class::SubjectMethods
|
26
|
+
|
27
|
+
# rubocop:disable Lint/HandleExceptions
|
28
|
+
begin
|
29
|
+
service_module = const_get(name.to_s.camelize)
|
30
|
+
include service_module
|
31
|
+
rescue NameError
|
32
|
+
end
|
33
|
+
# rubocop:enable Lint/HandleExceptions
|
34
|
+
end
|
35
|
+
|
36
|
+
def includes_external_services
|
37
|
+
includes(self.external_services.keys.map { |name| :"#{name}_service" })
|
38
|
+
end
|
39
|
+
|
40
|
+
def external_services_disabled
|
41
|
+
Thread.current[:external_services_disabled]
|
42
|
+
end
|
43
|
+
|
44
|
+
def external_services_disabled=(val)
|
45
|
+
Thread.current[:external_services_disabled] = val
|
46
|
+
end
|
47
|
+
|
48
|
+
def without_external_services
|
49
|
+
old = external_services_disabled
|
50
|
+
self.external_services_disabled = true
|
51
|
+
|
52
|
+
yield
|
53
|
+
ensure
|
54
|
+
self.external_services_disabled = old
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def get_service_class(name, options = {})
|
60
|
+
(options[:class] || "ExternalServices::#{name.to_s.camelize}").constantize
|
61
|
+
end
|
62
|
+
|
63
|
+
def define_external_service_getset(name, _options = {})
|
64
|
+
service_assoc = :"#{name}_service"
|
65
|
+
|
66
|
+
define_method :"#{name}_id" do
|
67
|
+
public_send(service_assoc).try(:public_send, :external_id)
|
68
|
+
end
|
69
|
+
|
70
|
+
define_method :"#{name}_id=" do |val|
|
71
|
+
public_send(service_assoc).try(:public_send, :external_id=, val)
|
72
|
+
end
|
73
|
+
|
74
|
+
define_method :"#{name}_extra_data" do
|
75
|
+
public_send(service_assoc).try(:public_send, :extra_data)
|
76
|
+
end
|
77
|
+
|
78
|
+
define_method :"#{name}_extra_data=" do |val|
|
79
|
+
public_send(service_assoc).try(:public_send, :extra_data=, val)
|
80
|
+
end
|
81
|
+
|
82
|
+
define_singleton_method :"find_by_#{name}_id" do |id|
|
83
|
+
all.joins(service_assoc).find_by(external_services: { external_id: id })
|
84
|
+
end
|
85
|
+
|
86
|
+
define_method service_assoc do
|
87
|
+
super() || public_send(:"build_#{service_assoc}")
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
92
|
+
def define_external_service_callbacks(name, options = {})
|
93
|
+
service_assoc = :"#{name}_service"
|
94
|
+
only_api_actions = (options[:only_api_actions] == true)
|
95
|
+
|
96
|
+
callbacks_module = Module.new do
|
97
|
+
extend ActiveSupport::Concern
|
98
|
+
|
99
|
+
included do
|
100
|
+
unless only_api_actions
|
101
|
+
before_save do
|
102
|
+
public_send(:"build_#{service_assoc}") if public_send(service_assoc).blank?
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
after_save :"#{name}_on_create", if: :id_changed?
|
107
|
+
|
108
|
+
after_save :"#{name}_on_update", if: proc {
|
109
|
+
!id_changed?
|
110
|
+
}
|
111
|
+
|
112
|
+
after_destroy :"#{name}_on_destroy"
|
113
|
+
end
|
114
|
+
|
115
|
+
define_method :"#{name}_on_create" do
|
116
|
+
public_send(service_assoc).on_subject_create(self) unless only_api_actions
|
117
|
+
end
|
118
|
+
protected :"#{name}_on_create"
|
119
|
+
|
120
|
+
define_method :"#{name}_on_update" do
|
121
|
+
public_send(service_assoc).on_subject_update(self) unless only_api_actions
|
122
|
+
end
|
123
|
+
|
124
|
+
define_method :"#{name}_on_destroy" do
|
125
|
+
public_send(service_assoc).on_subject_destroy(self) unless only_api_actions
|
126
|
+
end
|
127
|
+
protected :"#{name}_on_destroy"
|
128
|
+
|
129
|
+
define_method :"#{name}_on_revive" do
|
130
|
+
public_send(service_assoc).on_subject_revive(self) unless only_api_actions
|
131
|
+
end
|
132
|
+
protected :"#{name}_on_revive"
|
133
|
+
end
|
134
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
135
|
+
|
136
|
+
include callbacks_module
|
137
|
+
end
|
138
|
+
|
139
|
+
def define_external_service_sync_methods(name, _options = {})
|
140
|
+
service_assoc = :"#{name}_service"
|
141
|
+
synced_method = :"#{name}_service_synced?"
|
142
|
+
disabled_method = :"#{name}_api_disabled"
|
143
|
+
|
144
|
+
syncs_module = Module.new do
|
145
|
+
extend ActiveSupport::Concern
|
146
|
+
|
147
|
+
define_method synced_method do
|
148
|
+
public_send(service_assoc).external_id?
|
149
|
+
end
|
150
|
+
|
151
|
+
define_method :external_services_synced? do
|
152
|
+
result = (!defined?(super) || super())
|
153
|
+
result &&= public_send(synced_method) unless public_send(disabled_method)
|
154
|
+
result
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
include syncs_module
|
159
|
+
end
|
160
|
+
|
161
|
+
# rubocop:disable Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/AbcSize
|
162
|
+
def define_external_service_helper_methods(name, _options = {})
|
163
|
+
## subject class methods
|
164
|
+
helpers_class_module = Module.new do
|
165
|
+
define_method :"with_#{name}_api_for" do |synced: [], for_syncing: [], &b|
|
166
|
+
return if ([synced] + [for_syncing]).flatten.select(&:"#{name}_api_disabled").any?
|
167
|
+
|
168
|
+
unsynced = [synced].flatten.select { |o| o.send("#{name}_id").nil? }
|
169
|
+
if unsynced.any?
|
170
|
+
objects = unsynced.map { |o| "#{o.class.name} (id=#{o.id})" }.join(', ')
|
171
|
+
raise "[#{name}] Trying to work with an unsynced objects: #{objects}"
|
172
|
+
end
|
173
|
+
|
174
|
+
b.call
|
175
|
+
end
|
176
|
+
|
177
|
+
define_method :"#{name}_api_name" do
|
178
|
+
to_s.demodulize.underscore
|
179
|
+
end
|
180
|
+
|
181
|
+
define_method :"#{name}_api_path" do
|
182
|
+
send(:"#{name}_api_name").pluralize
|
183
|
+
end
|
184
|
+
|
185
|
+
define_method :"#{name}_api_disabled" do
|
186
|
+
ENV["#{name}_api_disabled".upcase] == 'true' || Thread.current[:"#{name}_api_disabled"] || external_services_disabled
|
187
|
+
end
|
188
|
+
|
189
|
+
define_method :"#{name}_api_disabled=" do |val|
|
190
|
+
Thread.current[:"#{name}_api_disabled"] = val
|
191
|
+
end
|
192
|
+
|
193
|
+
define_method :"without_#{name}_api" do |&blk|
|
194
|
+
begin
|
195
|
+
old = send :"#{name}_api_disabled"
|
196
|
+
send :"#{name}_api_disabled=", true
|
197
|
+
|
198
|
+
blk.call
|
199
|
+
ensure
|
200
|
+
send :"#{name}_api_disabled=", old
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
## subject methods
|
206
|
+
helpers_module = Module.new do
|
207
|
+
define_method :"#{name}_api_disabled" do
|
208
|
+
self.class.send :"#{name}_api_disabled"
|
209
|
+
end
|
210
|
+
|
211
|
+
define_method :"#{name}_api_path" do
|
212
|
+
if send(:"#{name}_id").present?
|
213
|
+
"#{self.class.send(:"#{name}_api_path")}/#{send(:"#{name}_id")}"
|
214
|
+
else
|
215
|
+
self.class.send(:"#{name}_api_path")
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
define_method :"#{name}_api_data" do
|
220
|
+
send(:"to_#{name}_api")
|
221
|
+
end
|
222
|
+
|
223
|
+
define_method :"#{name}_api_action" do |method, **args|
|
224
|
+
return if self.class.send(:"#{name}_api_disabled")
|
225
|
+
return if !args[:force] && send(:"#{name}_api_disabled")
|
226
|
+
|
227
|
+
path = args[:path] || send(:"#{name}_api_path")
|
228
|
+
data = args[:data] || send(:"#{name}_api_data")
|
229
|
+
options = args[:options] || {}
|
230
|
+
|
231
|
+
options[:change_external_id] = true if options[:change_external_id].nil?
|
232
|
+
|
233
|
+
"ExternalServices::ApiActions::#{name.to_s.camelize}".constantize.create!(
|
234
|
+
initiator: self,
|
235
|
+
name: args[:name] || self.class.send(:"#{name}_api_name"),
|
236
|
+
method: method,
|
237
|
+
path: path,
|
238
|
+
data: data,
|
239
|
+
queue: args[:queue],
|
240
|
+
options: options
|
241
|
+
)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
extend helpers_class_module
|
246
|
+
include helpers_module
|
247
|
+
end
|
248
|
+
# rubocop:enable Metrics/MethodLength, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/AbcSize
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
ActiveSupport.on_load :active_record do
|
255
|
+
ActiveRecord::Base.send :include, ExternalServices::ActiveRecord::HasExternalService
|
256
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module ExternalServices
|
2
|
+
module Api
|
3
|
+
class Error < StandardError
|
4
|
+
def initialize(response)
|
5
|
+
@response = response
|
6
|
+
end
|
7
|
+
|
8
|
+
def message
|
9
|
+
@response.inspect
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def api_url
|
14
|
+
ENV["#{to_s.demodulize.upcase}_API_URL"]
|
15
|
+
end
|
16
|
+
|
17
|
+
def api_key
|
18
|
+
ENV["#{to_s.demodulize.upcase}_API_KEY"]
|
19
|
+
end
|
20
|
+
|
21
|
+
def auth_header
|
22
|
+
'Authorization'
|
23
|
+
end
|
24
|
+
|
25
|
+
def connection
|
26
|
+
return if api_url.blank?
|
27
|
+
|
28
|
+
@connection ||= Faraday.new(url: api_url) do |f|
|
29
|
+
f.request :json
|
30
|
+
f.response :json
|
31
|
+
|
32
|
+
f.adapter :net_http
|
33
|
+
|
34
|
+
f.headers['Content-Type'] = 'application/json'
|
35
|
+
f.headers['Accept'] = 'application/json'
|
36
|
+
f.headers[auth_header] = api_key
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def get(path, params: {}, **_kwargs)
|
41
|
+
request(:get, path, params)
|
42
|
+
end
|
43
|
+
|
44
|
+
def post(path, body:, params: {}, **_kwargs)
|
45
|
+
request(:post, path, params, body)
|
46
|
+
end
|
47
|
+
|
48
|
+
def put(path, body:, params: {}, **_kwargs)
|
49
|
+
request(:put, path, params, body)
|
50
|
+
end
|
51
|
+
|
52
|
+
def delete(path, params: {}, **_kwargs)
|
53
|
+
request(:delete, path, params)
|
54
|
+
end
|
55
|
+
|
56
|
+
def fake?
|
57
|
+
!connection
|
58
|
+
end
|
59
|
+
|
60
|
+
def fake_response_body(method, path, params = {})
|
61
|
+
(method == :post ? { 'id' => SecureRandom.hex } : {})
|
62
|
+
end
|
63
|
+
|
64
|
+
def request(method, path, params = {}, body = nil)
|
65
|
+
resp = if fake?
|
66
|
+
Struct.new(:success?, :body, :headers).new(true, fake_response_body(method, path, params), {})
|
67
|
+
else
|
68
|
+
connection.send(method, path, body) do |req|
|
69
|
+
req.params = params
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
raise Error, resp unless resp.blank? || resp.success?
|
74
|
+
|
75
|
+
resp
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'external_services/engine'
|
2
|
+
require 'external_services/version'
|
3
|
+
|
4
|
+
require 'external_services/active_record'
|
5
|
+
require 'external_services/api'
|
6
|
+
require 'generators/install_generator'
|
7
|
+
require 'generators/service_generator'
|
8
|
+
|
9
|
+
Dir[File.join(File.expand_path('lib/external_services'), 'api', '*.rb')].each do |api|
|
10
|
+
require api
|
11
|
+
end
|
12
|
+
|
13
|
+
module ExternalServices
|
14
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/active_record'
|
3
|
+
|
4
|
+
module ExternalServices
|
5
|
+
module Generators
|
6
|
+
# Installs ExternalServices in a rails app.
|
7
|
+
class InstallGenerator < ::Rails::Generators::Base
|
8
|
+
include ::Rails::Generators::Migration
|
9
|
+
|
10
|
+
source_root File.expand_path('../templates', __FILE__)
|
11
|
+
|
12
|
+
desc 'Generates migrations and directories.'
|
13
|
+
|
14
|
+
def create_migration_files
|
15
|
+
add_migration('create_external_services')
|
16
|
+
add_migration('create_external_services_api_actions')
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_directories
|
20
|
+
create_directory 'app/models/external_services/api_actions'
|
21
|
+
create_directory 'app/jobs/external_services'
|
22
|
+
create_directory 'lib/external_services/api'
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.next_migration_number(dirname)
|
26
|
+
::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
27
|
+
end
|
28
|
+
|
29
|
+
protected
|
30
|
+
|
31
|
+
def create_directory(dir)
|
32
|
+
dir = File.expand_path(dir)
|
33
|
+
empty_directory dir
|
34
|
+
add_file File.join(dir, '.keep')
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_migration(template)
|
38
|
+
migration_dir = File.expand_path('db/migrate')
|
39
|
+
if self.class.migration_exists?(migration_dir, template)
|
40
|
+
::Kernel.warn "Migration already exists: #{template}"
|
41
|
+
else
|
42
|
+
migration_template "migrations/#{template}.rb", "db/migrate/#{template}.rb"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/active_record'
|
3
|
+
|
4
|
+
module ExternalServices
|
5
|
+
module Generators
|
6
|
+
# Installs ExternalServices in a rails app.
|
7
|
+
class ServiceGenerator < ::Rails::Generators::NamedBase
|
8
|
+
source_root File.expand_path('../templates/services', __FILE__)
|
9
|
+
|
10
|
+
|
11
|
+
class_option(
|
12
|
+
:only_api_actions,
|
13
|
+
type: :boolean,
|
14
|
+
default: false,
|
15
|
+
desc: "Do not generate service model class"
|
16
|
+
)
|
17
|
+
|
18
|
+
desc 'Generates specified model and API classes.'
|
19
|
+
|
20
|
+
def add_model
|
21
|
+
return if options.only_api_actions?
|
22
|
+
|
23
|
+
dir = File.expand_path('app/models/external_services')
|
24
|
+
template 'model.rb', File.join(dir, "#{file_name}.rb")
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_api_action
|
28
|
+
dir = File.expand_path('app/models/external_services/api_actions')
|
29
|
+
template 'api_action.rb', File.join(dir, "#{file_name}.rb")
|
30
|
+
end
|
31
|
+
|
32
|
+
def add_api
|
33
|
+
dir = File.expand_path('lib/external_services/api')
|
34
|
+
template 'api.rb', File.join(dir, "#{file_name}.rb")
|
35
|
+
end
|
36
|
+
|
37
|
+
def add_api_job
|
38
|
+
dir = File.expand_path('app/jobs/external_services')
|
39
|
+
template 'api_job.rb', File.join(dir, "#{file_name}_api_job.rb")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class CreateExternalServices < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :external_services do |t|
|
4
|
+
t.references :subject, null: false, polymorphic: true, index: true
|
5
|
+
|
6
|
+
t.string :type, null: false
|
7
|
+
t.string :external_id
|
8
|
+
t.text :extra_data
|
9
|
+
end
|
10
|
+
add_index :external_services, :external_id
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class CreateExternalServicesApiActions < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :external_services_api_actions do |t|
|
4
|
+
t.references :initiator, null: false, polymorphic: true, index: { name: 'esaa_on_initiator_type_and_initiator_id' }
|
5
|
+
t.string :type, null: false
|
6
|
+
|
7
|
+
t.string :name
|
8
|
+
t.string :method, null: false
|
9
|
+
t.string :path, null: false
|
10
|
+
t.text :data
|
11
|
+
t.string :signature
|
12
|
+
t.string :queue, null: false
|
13
|
+
|
14
|
+
t.text :options
|
15
|
+
|
16
|
+
t.timestamp :created_at
|
17
|
+
t.timestamp :processed_at
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ExternalServices
|
2
|
+
module Api
|
3
|
+
module <%= name %>
|
4
|
+
extend ExternalServices::Api
|
5
|
+
class Error < ExternalServices::Api::Error; end
|
6
|
+
|
7
|
+
# module_function
|
8
|
+
#
|
9
|
+
# Redefine everything unusual:
|
10
|
+
# def api_key
|
11
|
+
# 'my hardcoded key'
|
12
|
+
# end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: external_services
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sergey Gnuskov
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-07-29 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: faraday
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ! '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.9'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ! '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.9'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rails
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 4.2.5
|
34
|
+
- - <
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '6.0'
|
37
|
+
type: :runtime
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 4.2.5
|
44
|
+
- - <
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '6.0'
|
47
|
+
description:
|
48
|
+
email:
|
49
|
+
- sergey.gnuskov@flant.com
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- .gitignore
|
55
|
+
- .overcommit.yml
|
56
|
+
- .rspec
|
57
|
+
- .rubocop.yml
|
58
|
+
- .travis.yml
|
59
|
+
- Gemfile
|
60
|
+
- LICENSE.txt
|
61
|
+
- README.md
|
62
|
+
- Rakefile
|
63
|
+
- app/jobs/external_services/api_job.rb
|
64
|
+
- app/models/external_services/api_action.rb
|
65
|
+
- app/models/external_services/service.rb
|
66
|
+
- bin/rails
|
67
|
+
- external_services.gemspec
|
68
|
+
- lib/external_services.rb
|
69
|
+
- lib/external_services/active_record.rb
|
70
|
+
- lib/external_services/api.rb
|
71
|
+
- lib/external_services/engine.rb
|
72
|
+
- lib/external_services/version.rb
|
73
|
+
- lib/generators/install_generator.rb
|
74
|
+
- lib/generators/service_generator.rb
|
75
|
+
- lib/generators/templates/migrations/create_external_services.rb
|
76
|
+
- lib/generators/templates/migrations/create_external_services_api_actions.rb
|
77
|
+
- lib/generators/templates/services/api.rb
|
78
|
+
- lib/generators/templates/services/api_action.rb
|
79
|
+
- lib/generators/templates/services/api_job.rb
|
80
|
+
- lib/generators/templates/services/model.rb
|
81
|
+
homepage: https://github.com/flant/external_services
|
82
|
+
licenses:
|
83
|
+
- MIT
|
84
|
+
metadata: {}
|
85
|
+
post_install_message:
|
86
|
+
rdoc_options: []
|
87
|
+
require_paths:
|
88
|
+
- lib
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ! '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
requirements: []
|
100
|
+
rubyforge_project:
|
101
|
+
rubygems_version: 2.4.5
|
102
|
+
signing_key:
|
103
|
+
specification_version: 4
|
104
|
+
summary: Gem helps syncronizing objects to different external services like Gitlab,
|
105
|
+
Redmine and any other.
|
106
|
+
test_files: []
|