integrative 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a841fed65f619a6170d26d8816ff8f697d39c463
4
+ data.tar.gz: 906a568cf159349a97f435714a0989f1c1dc36db
5
+ SHA512:
6
+ metadata.gz: 034c141465f37b82b4b4449f97cef1ced395a367d52fe9109d172e9c36a199f3baf068d618c527722cbcb8b32aded0b2e916ade2f511a509d4ab30368d9c6c74
7
+ data.tar.gz: 77c8c554abe0adcb2edfb661677c7e8200e78734c365256c1dc1d324437ff8d8a4641f2cf3045fa59a3d1164a2b82e19cf627d30cb2d561c885fb9cefaae3e99
data/README.md ADDED
@@ -0,0 +1,191 @@
1
+ # Integrative
2
+
3
+ Integrative is a library for integrating external resources into ActiveRecord models.
4
+
5
+ Now, however you interpret "external" - this library is exactly for that ;-)
6
+
7
+ Cosider few exaples of what can be integrated into ActiveRecord model:
8
+ * ActiveResource model
9
+ * a custom object that fetches data from external websites
10
+ * an object fetching data from Redis
11
+ * another ActiveRecord model
12
+
13
+ You may ask
14
+
15
+ > :triumph:: ok, but why would I use Integrative? I can easily implement that on my own.
16
+
17
+ > :sunglasses:: I'm glad you asked. The best reason is that **it helps to fetch a lot of data at once**, and by that it significantly improves performance.
18
+
19
+ ## Examples
20
+ ### Example 1: Another data store
21
+
22
+ Imagine the following context:
23
+
24
+ ```ruby
25
+ class User < ApplicationRecord
26
+ include Integrative::Integrator
27
+
28
+ integrates :user_flag
29
+ end
30
+
31
+ class UserFlag < SomeRedisObject
32
+ include Integrative::Integrated
33
+
34
+ attr_accessor :user_id
35
+ attr_accessor :name
36
+
37
+ def self.find(ids)
38
+ # Have in mind it's a simplification.
39
+ # `find` should return array of hashes
40
+ # with (in this case) `name` and `user_id`
41
+ # so you'd need to store hashes
42
+ # and convert data accordingly
43
+ @redis.mget(*ids)
44
+ end
45
+ end
46
+ ```
47
+
48
+ Now let's say you would like to see the list of all users with their flags. Try this:
49
+
50
+ ```ruby
51
+ users = User.limit(1000).integrate(:user_flag).to_a
52
+ ```
53
+
54
+ **the above code will call redis only once** and will fetch user_flag for all 1000 users,
55
+ so now you can access all the flags like this:
56
+
57
+ ```ruby
58
+ users.map { |user| user.user_flag.name }
59
+ ```
60
+
61
+ ### Example 2: Prefetching another Active Record model
62
+
63
+ You can use Integrative also when you want to eager-load certain models to collection of other models when `ActiveRecord` doesn't make it easy.
64
+ Let's say you have the following situation:
65
+
66
+ ```ruby
67
+ class User < ApplicationRecord
68
+ include Integrative::Integrator
69
+
70
+ integrates :relation, requires: [:with]
71
+ end
72
+
73
+ class Relation
74
+ include Integrative::Integrated
75
+
76
+ def self.integrative_find(ids, integration)
77
+ Relation.where(user_id: integration.call_options[:with].id, other_user_id: ids)
78
+ end
79
+ end
80
+ ```
81
+ Now you want to fetch some Users and have already prefatched information about their relation with the current user.
82
+
83
+ With `Integrative` you just do:
84
+
85
+ ```ruby
86
+ User.where(public: true).integrate(:relation, with: current_user).limit(1000)
87
+ ```
88
+
89
+ Boom. Pretty cool, ha?
90
+
91
+ ## Treating integrated object as primary type value (string, int, ...)
92
+
93
+ Now check this out:
94
+
95
+ ```ruby
96
+ class User < ApplicationRecord
97
+ integrates :is_admin, as: :primary
98
+ end
99
+
100
+ User.integrate(:is_admin).first.is_admin # that would be `true` or `false`
101
+ ```
102
+
103
+ Of course for that you'd need to take care for preparing data properly in the integrated object:
104
+
105
+ ```ruby
106
+ class IsAdmin
107
+ include Integrative::Integrated
108
+
109
+ def self.integrative_find(ids, integration)
110
+ # this should return a list of hashes
111
+ # with a key (e.g. user_id) and a `value`,
112
+ # for example:
113
+ # [
114
+ # {user_id: 1, value: true}
115
+ # {user_id: 2, value: false}
116
+ # ]
117
+ response = find(ids)
118
+ response.map { |item| OpenStruct.new(item) }
119
+ end
120
+ end
121
+ ```
122
+
123
+ ## Integrating objects with `1-to-many` relation
124
+
125
+ Like with `has_one` and `has_many` relations, sometimes you want to assign one external object
126
+ per model, but sometimes you want to assign an array of external objects per model. In such moments use `array: true` as an option parameter of integration
127
+
128
+ ```ruby
129
+ class User < ApplicationRecord
130
+ integrates :flags, array: true
131
+ end
132
+
133
+ User.first.flags # this is an array
134
+ ```
135
+
136
+ ## Using `Integrative` on a single instance
137
+
138
+ So what if you'd like to prefetch something not for a list of users, but for a single user?
139
+ Well, it works exactly how you would think:
140
+
141
+ ```ruby
142
+ user = User.first
143
+ user.flags # yes, that's gonna fetch and return a list of flags of the user.
144
+ ```
145
+
146
+ ## Using `Integrative` on an array
147
+
148
+ Sometimes you just want to prefetch certain data for an array of objects (and not for `ActiveRecord::Relation`). In such case just do:
149
+
150
+ ```ruby
151
+ users_with_flags = Integrative.integrate_into(users, :user_flags)
152
+ ```
153
+
154
+ ## Working with external resources
155
+
156
+ While working with external resources you need to implement the code that fetches external data and then assigns parts of it to the right models. Now it's all up to you how you'll do this but there is a pattern that fits well into `Integrative`. Take a look:
157
+
158
+ ```ruby
159
+ # file app/models/integrative_record.rb
160
+ class IntegrativeRecord
161
+ include Integrative::Integrated
162
+
163
+ def url_base
164
+ 'http://external.service.com'
165
+ end
166
+
167
+ def full_url(ids)
168
+ url_base + path(ids)
169
+ end
170
+ end
171
+
172
+ # file app/models/avatar.rb
173
+ class Avatar < IntegrativeRecord
174
+
175
+ def path(ids)
176
+ "avatars?user_ids=#{ids.join(',')}"
177
+ end
178
+
179
+ def find(ids)
180
+ response = RestClient.get full_path(ids)
181
+ response_hash = HashWithIndifferentAccess.new(JSON.parse(response.body))
182
+ response_hash[:results]
183
+ end
184
+ end
185
+ ```
186
+
187
+ ## Contributing
188
+
189
+ If you feel like contributing to this project, feel free to create a bug report or send a pull request, but if you want to increase chances that I'll find time for taking care for your contribution, please make sure to make it easy for me - for pull requests write tests, for bug reports attach code that will let me reproduce the issue.
190
+
191
+ Have fun ;-)
data/Rakefile ADDED
@@ -0,0 +1,34 @@
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 = 'Integrative'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+
21
+
22
+ require 'bundler/gem_tasks'
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'lib'
28
+ t.libs << 'test'
29
+ t.pattern = 'test/**/*_test.rb'
30
+ t.verbose = false
31
+ end
32
+
33
+
34
+ task default: :test
@@ -0,0 +1,35 @@
1
+ # Integrative lets you add objects to ActiveRecord relation or to an array
2
+ #
3
+ # class User < ApplicationRecord
4
+ # integrates :relation, require: [:with]
5
+ # end
6
+ #
7
+ # class Relation
8
+ # include Integrative::Integrated
9
+ #
10
+ # attr_accessor :user_id
11
+ # attr_accessor :kind
12
+ #
13
+ # ...
14
+ # end
15
+ #
16
+ # Now let's say you would like to have the list of all users with their relations.
17
+ # Try this:
18
+ #
19
+ # users = User.limit(1000).integrate(:relation, with: current_user).to_a
20
+ #
21
+ # and the integration of relations will happen for the whole collection and not
22
+ # just for individual users.
23
+ require 'integrative/utils'
24
+
25
+ module Integrative
26
+ extend Utils
27
+
28
+ autoload :Integrator, 'integrative/integrator'
29
+ autoload :Integrated, 'integrative/integrated'
30
+ autoload :Integration, 'integrative/integration'
31
+ autoload :Errors, 'integrative/errors'
32
+ autoload :Extensions, 'integrative/extensions'
33
+ end
34
+
35
+
@@ -0,0 +1,76 @@
1
+ module Integrative
2
+ module Errors
3
+ class IntegrationError < StandardError
4
+ attr_accessor :integration
5
+
6
+ def initialize(message, integration)
7
+ @integration = integration
8
+ super(message)
9
+ end
10
+ end
11
+
12
+ class RuntimeOptionMissingError < IntegrationError
13
+ def initialize(integration)
14
+ message = "You used 'integrate' for #{integration.name} without options," +
15
+ " but the following options are required: " +
16
+ " #{integration.init_options[:requires]}"
17
+ super(message, integration)
18
+ end
19
+ end
20
+
21
+ class UnexpectedRuntimeOptionError < IntegrationError
22
+ def initialize(integration)
23
+ required = integration.call_options.keys
24
+ message = "You used 'integrate' for #{integration.name} with unexpected options," +
25
+ " you should define integration like this:" +
26
+ " 'integrates :#{integration.name}, requires: [:#{required.join(", :")}]'"
27
+ super(message, integration)
28
+ end
29
+ end
30
+
31
+ class TooManyRuntimeOptionsError < IntegrationError
32
+ def initialize(integration, unexpected_options)
33
+ message = "You used 'integrate' for :#{integration.name}" +
34
+ " on #{integration.integrator_class.name}" +
35
+ " with too many options: #{unexpected_options}"
36
+ super(message, integration)
37
+ end
38
+ end
39
+
40
+ class TooLittleRuntimeOptionsError < IntegrationError
41
+ def initialize(integration, missing_options)
42
+ message = "You used 'integrate' for :#{integration.name}" +
43
+ " on #{integration.integrator_class.name}" +
44
+ " with too little options: #{missing_options}"
45
+ super(message, integration)
46
+ end
47
+ end
48
+
49
+ class IntegratorError < StandardError
50
+ attr_accessor :integration
51
+
52
+ def initialize(message, integrator)
53
+ @integrator = integrator
54
+ super(message)
55
+ end
56
+ end
57
+
58
+ class MethodAlreadyExistsError < IntegratorError
59
+ def initialize(integrator, name)
60
+ message = "Method '#{name}' is already defined on #{integrator.name}." +
61
+ " You can not define integration with this name."
62
+ super(message, integrator)
63
+ end
64
+ end
65
+
66
+ class IntegrationDefinitionMissingError < IntegratorError
67
+ def initialize(integrator, names)
68
+ message = "You tried to call `integrate` on a class #{integrator.name}" +
69
+ " but this class doesn't have this integration." +
70
+ " add the following line to the class #{integrator.name}:" +
71
+ " 'integrates :#{names.join(', :')}'"
72
+ super(message, integrator)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,7 @@
1
+ module Integrative
2
+ module Extensions
3
+ autoload :RelationExtension, 'integrative/extensions/relation_extension'
4
+ end
5
+ end
6
+
7
+
@@ -0,0 +1,37 @@
1
+ module Integrative
2
+ module Extensions
3
+ module RelationExtension
4
+ def integrate(*name_or_names, **options)
5
+ names = [*name_or_names]
6
+ names.each do |name|
7
+ integrate_per_name(name, options)
8
+ end
9
+ self
10
+ end
11
+
12
+ def load
13
+ super
14
+ if @integrations_used.present?
15
+ Rails.logger.info "Integrations fetched for #{@records.length} #{klass.name} records."
16
+ @integrations_used.each do |integration|
17
+ integration.integrated_class.integrative_find_and_assign(@records, integration)
18
+ end
19
+ end
20
+ self
21
+ end
22
+
23
+ private
24
+
25
+ def integrate_per_name(name, options)
26
+ integration = klass.integrations_defined.find { |i| i.name == name }
27
+ if integration.nil?
28
+ raise Errors::IntegrationDefinitionMissingError.new(klass, [name])
29
+ end
30
+ integration.call_options = options
31
+ integration.invalidate
32
+ @integrations_used ||= []
33
+ @integrations_used << integration
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,62 @@
1
+ require 'ostruct'
2
+
3
+ module Integrative
4
+ module Integrated
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ def integrative_find(ids, options)
9
+ find(ids)
10
+ end
11
+
12
+ def integrator_ids(integrator_records, integration)
13
+ integrator_records.map(&integration.integrator_key)
14
+ end
15
+
16
+ def integrative_find_and_assign(integrator_records, integration)
17
+ ids = integrator_ids(integrator_records, integration)
18
+ integrated = integrative_find(ids, integration)
19
+ integrated_by_integrator_id = array_to_hash(integrated, integration)
20
+ integrator_records.each do |record|
21
+ record.public_send(integration.setter, integrated_by_integrator_id[record.id])
22
+ end
23
+ end
24
+
25
+ def integrative_value(object, integration)
26
+ if [:primary, :value, :simple].include? integration.init_options[:as]
27
+ object[:value]
28
+ else
29
+ object
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def array_to_hash(array, integration)
36
+ if integration.init_options[:array]
37
+ array_to_hash_as_array(array, integration)
38
+ else
39
+ array_to_hash_as_value(array, integration)
40
+ end
41
+ end
42
+
43
+ def array_to_hash_as_value(array, integration)
44
+ result = array.map do |object|
45
+ key = object.public_send(integration.integrated_key)
46
+ [key, integrative_value(object, integration)]
47
+ end
48
+ Hash[result]
49
+ end
50
+
51
+ def array_to_hash_as_array(array, integration)
52
+ result = {}
53
+ array.each do |object|
54
+ key = object.public_send(integration.integrated_key)
55
+ result[key] ||= []
56
+ result[key] << integrative_value(object, integration)
57
+ end
58
+ result
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,52 @@
1
+ module Integrative
2
+ class Integration
3
+ attr_accessor :name
4
+ attr_accessor :integrator_class
5
+ attr_accessor :integrated_class
6
+ attr_accessor :init_options
7
+ attr_accessor :call_options
8
+
9
+ def initialize(name, integrator_class, options)
10
+ @name = name
11
+ @integrator_class = integrator_class
12
+ @integrated_class = name.to_s.camelize.singularize.constantize
13
+ @init_options = options
14
+ end
15
+
16
+ def invalidate
17
+ if call_options.blank? && init_options[:requires].present?
18
+ raise Errors::RuntimeOptionMissingError.new(self)
19
+ end
20
+
21
+ if call_options.present? && init_options[:requires].blank?
22
+ raise Errors::UnexpectedRuntimeOptionError.new(self)
23
+ end
24
+
25
+ if call_options.present? && init_options[:requires].present?
26
+ unexpected_options = call_options.keys - init_options[:requires]
27
+ missing_options = init_options[:requires] - call_options.keys
28
+
29
+ if unexpected_options.present?
30
+ raise Errors::TooManyRuntimeOptionsError.new(self, unexpected_options)
31
+ end
32
+
33
+ if missing_options.present?
34
+ raise Errors::TooLittleRuntimeOptionsError.new(self, missing_options)
35
+ end
36
+ end
37
+ end
38
+
39
+ def setter
40
+ "#{name}="
41
+ end
42
+
43
+ def integrator_key
44
+ init_options[:integrator_key] || :id
45
+ end
46
+
47
+ def integrated_key
48
+ default_integrated_key = "#{integrator_class.name.underscore}_id"
49
+ init_options[:integrated_key] || default_integrated_key
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,66 @@
1
+ module Integrative
2
+ module Integrator
3
+ extend ActiveSupport::Concern
4
+
5
+ def integrative_dynamic_method_call(name, integration)
6
+ ivar = "@#{name}"
7
+ if instance_variable_defined? ivar
8
+ instance_variable_get ivar
9
+ else
10
+ Rails.logger.info "Integrations fetched for a single #{self.class.name} record."
11
+ integration.integrated_class.integrative_find_and_assign([self], integration)
12
+ instance_variable_get ivar
13
+ end
14
+ end
15
+
16
+ class_methods do
17
+ def integrates(name, options = {})
18
+ initialize_integrative(name)
19
+ define_integration(name, options)
20
+ end
21
+
22
+ def integrate(*name_or_names, **options)
23
+ if all.public_methods.include? :integrate
24
+ all.integrate(*name_or_names, **options)
25
+ else
26
+ raise Errors::IntegrationDefinitionMissingError.new(self, name_or_names)
27
+ end
28
+ end
29
+
30
+ def patch_activerecord_relation_for_integrative
31
+ self::ActiveRecord_Relation.class_eval do
32
+ include Integrative::Extensions::RelationExtension
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def initialize_integrative(name)
39
+ if self.instance_methods.include? name
40
+ raise Errors::MethodAlreadyExistsError.new(self, name)
41
+ end
42
+ if !defined?(integrations_defined)
43
+ patch_activerecord_relation_for_integrative
44
+ class_attribute :integrations_defined
45
+ end
46
+ end
47
+
48
+ def define_integration(name, options)
49
+ integration = Integration.new(name, self, options)
50
+ self.integrations_defined ||= []
51
+ self.integrations_defined << integration
52
+ self.class_eval do
53
+ define_integration_method(name, integration)
54
+ end
55
+ end
56
+
57
+ def define_integration_method(name, integration)
58
+ attr_accessor name
59
+
60
+ define_method name do
61
+ integrative_dynamic_method_call(name, integration)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,10 @@
1
+ module Integrative
2
+ module Utils
3
+ def integrate_into(records, integration_name, options = {})
4
+ if records.length > 0
5
+ integration = Integration.new(integration_name, records.first.class, options)
6
+ integration.integrated_class.integrative_find_and_assign(records, integration)
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,3 @@
1
+ module Integrative
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :integrative do
3
+ # # Task goes here
4
+ # end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: integrative
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Krzysiek Herod
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-07-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 5.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 5.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: sqlite3
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: factory_girl
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Integrative is a library for integrating external resources into ActiveRecord
56
+ models.
57
+ email:
58
+ - krzysiek.herod@gmail.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - README.md
64
+ - Rakefile
65
+ - lib/integrative.rb
66
+ - lib/integrative/errors.rb
67
+ - lib/integrative/extensions.rb
68
+ - lib/integrative/extensions/relation_extension.rb
69
+ - lib/integrative/integrated.rb
70
+ - lib/integrative/integration.rb
71
+ - lib/integrative/integrator.rb
72
+ - lib/integrative/utils.rb
73
+ - lib/integrative/version.rb
74
+ - lib/tasks/integrative_tasks.rake
75
+ homepage: https://github.com/netizer/integrative
76
+ licenses:
77
+ - MIT
78
+ metadata: {}
79
+ post_install_message:
80
+ rdoc_options: []
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - ">="
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: '0'
93
+ requirements: []
94
+ rubyforge_project:
95
+ rubygems_version: 2.6.6
96
+ signing_key:
97
+ specification_version: 4
98
+ summary: Integrative is a library for integrating external resources into ActiveRecord
99
+ models.
100
+ test_files: []