external_services 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /spec/dummy/log/
11
+ /spec/dummy/tmp/
12
+ /spec/dummy/db/*.sqlite3
13
+ /spec/dummy/db/schema.rb
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
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
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,11 @@
1
+ require 'rails'
2
+ module ExternalServices
3
+ class Engine < ::Rails::Engine
4
+ isolate_namespace ExternalServices
5
+
6
+ config.generators do |g|
7
+ g.test_framework :rspec
8
+ g.fixture_replacement :factory_girl, dir: 'spec/factories'
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module ExternalServices
2
+ VERSION = '0.1.0'.freeze
3
+ 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
@@ -0,0 +1,9 @@
1
+ module ExternalServices
2
+ module ApiActions
3
+ class <%= name %> < ApiAction
4
+ # def execute!
5
+ # # TODO
6
+ # end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,4 @@
1
+ module ExternalServices
2
+ class <%= name %>ApiJob < ApiJob
3
+ end
4
+ end
@@ -0,0 +1,9 @@
1
+ module ExternalServices
2
+ class <%= name %> < Service
3
+ module SubjectClassMethods
4
+ end
5
+
6
+ module SubjectMethods
7
+ end
8
+ end
9
+ 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: []