remote-resource 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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +19 -0
  3. data/.gitignore +9 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +1156 -0
  6. data/.travis.yml +17 -0
  7. data/Gemfile +15 -0
  8. data/Guardfile +17 -0
  9. data/LICENSE.txt +21 -0
  10. data/Procfile.dev +5 -0
  11. data/README.md +314 -0
  12. data/Rakefile +12 -0
  13. data/bin/console +15 -0
  14. data/bin/setup +8 -0
  15. data/lib/remote_resource/association_builder.rb +47 -0
  16. data/lib/remote_resource/attribute_http_client.rb +41 -0
  17. data/lib/remote_resource/attribute_key.rb +26 -0
  18. data/lib/remote_resource/attribute_method_attacher.rb +117 -0
  19. data/lib/remote_resource/attribute_specification.rb +51 -0
  20. data/lib/remote_resource/attribute_storage_value.rb +63 -0
  21. data/lib/remote_resource/base/attributes.rb +44 -0
  22. data/lib/remote_resource/base/base_class_methods.rb +23 -0
  23. data/lib/remote_resource/base/dsl.rb +27 -0
  24. data/lib/remote_resource/base/rescue.rb +43 -0
  25. data/lib/remote_resource/base.rb +35 -0
  26. data/lib/remote_resource/bridge.rb +174 -0
  27. data/lib/remote_resource/configuration/logger.rb +24 -0
  28. data/lib/remote_resource/configuration/lookup_method.rb +24 -0
  29. data/lib/remote_resource/configuration/storage.rb +24 -0
  30. data/lib/remote_resource/errors.rb +40 -0
  31. data/lib/remote_resource/log_subscriber.rb +39 -0
  32. data/lib/remote_resource/lookup/default.rb +39 -0
  33. data/lib/remote_resource/notifications.rb +17 -0
  34. data/lib/remote_resource/railtie.rb +21 -0
  35. data/lib/remote_resource/scope_evaluator.rb +52 -0
  36. data/lib/remote_resource/storage/cache_control.rb +120 -0
  37. data/lib/remote_resource/storage/db_cache.rb +36 -0
  38. data/lib/remote_resource/storage/db_cache_factory.rb +38 -0
  39. data/lib/remote_resource/storage/memory.rb +27 -0
  40. data/lib/remote_resource/storage/null_storage_entry.rb +43 -0
  41. data/lib/remote_resource/storage/redis.rb +27 -0
  42. data/lib/remote_resource/storage/serializer.rb +15 -0
  43. data/lib/remote_resource/storage/serializers/marshal.rb +18 -0
  44. data/lib/remote_resource/storage/storage_entry.rb +69 -0
  45. data/lib/remote_resource/version.rb +3 -0
  46. data/lib/remote_resource.rb +34 -0
  47. data/remote-resource.gemspec +27 -0
  48. metadata +175 -0
@@ -0,0 +1,35 @@
1
+ require 'active_support/descendants_tracker'
2
+
3
+ module RemoteResource
4
+ # the base class for defining an api.
5
+ class Base
6
+ extend ActiveSupport::DescendantsTracker
7
+
8
+ extend RemoteResource::BaseClassMethods
9
+ extend RemoteResource::Dsl
10
+
11
+ include RemoteResource::Attributes
12
+ include RemoteResource::Rescue
13
+
14
+ attr_reader :scope
15
+
16
+ def initialize(**args)
17
+ @scope = args
18
+ create_attributes(self)
19
+ AttributeMethodAttacher.new(self.class).attach_to(self.class)
20
+ end
21
+
22
+ def client
23
+ self.class.client_proc.call(@scope)
24
+ end
25
+
26
+ def resource(name = :default, resource_client = nil)
27
+ if (attr_resource = self.class.resources[name])
28
+ attr_resource.call(resource_client || client, @scope)
29
+ else
30
+ msg = "there is no resource named `#{name}` on #{self.class.name}."
31
+ fail ArgumentError, msg
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,174 @@
1
+ require 'remote_resource/attribute_method_attacher'
2
+
3
+ module RemoteResource
4
+ # Extending a class with the HasRemote::Bridge module will bring in
5
+ # the has_remote and embed_remote method, and therefore all of the
6
+ # functionality of the has_remote gem. In Rails, ActiveRecord is
7
+ # automatically extended with Bridge, and you would use the has_remote call
8
+ # like a familiar 'has_one' association method. Outside of Rails, or if used
9
+ # in non ActiveRecord domain objects, extending your domain objects with
10
+ # Bridge is how to setup has_remote on those classes.
11
+ module Bridge
12
+ # Public: Setup the provided remote resource on this class. This creates a
13
+ # method on the class that returns the remote resource object that is
14
+ # associated with that record. The method shares the name with the first
15
+ # argument unless the `as` options is supplied to override this.
16
+ #
17
+ # which_class - A symbol representing a descendant class of
18
+ # HasRemote::Resource. The symbol is the underscored
19
+ # version of the class name.
20
+ # Ex: (class) GithubUser => (symbol) :github_user
21
+ # options - A hash of options. (default: {}) Always use symbols for
22
+ # keys. These options are also passed to the
23
+ # AssociationBuilder and the AttributeMethodAttacher.
24
+ # :as - a symbol that defines the method name that
25
+ # returns the association. By default, this
26
+ # is the same name as the first argument
27
+ # specifying the remote resource class.
28
+ # :scope - the scope option represents the context in
29
+ # which this api resource is unique. It has
30
+ # a similar meaning to ActiveRecord's
31
+ # meaning of scope, as opposed the API
32
+ # access meaning. The scope value can be a
33
+ # Symbol, Array, or Hash. It is used to
34
+ # build the scope argument, which is sent
35
+ # into the client and resource blocks on the
36
+ # Base attributes class. This argument to
37
+ # these blocks is always a hash, whose
38
+ # values were methods responses evaluated on
39
+ # the target_class.
40
+ # :prefix - prefix for the names of the newly created
41
+ # methods.
42
+ # :attributes_map - A hash for overriding method names to
43
+ # create. The keys in this hash represent an
44
+ # attribute defined on the resource. The
45
+ # values are the overriding name of the
46
+ # method.
47
+ #
48
+ # Examples
49
+ #
50
+ # class Repo < ActiveRecord::Base
51
+ # has_remote :github_repo
52
+ #
53
+ # ...
54
+ # end
55
+ #
56
+ # The above `has_remote` call assumes that the following class
57
+ # exists:
58
+ #
59
+ # class GithubRepo < HasRemote::Resource
60
+ # ...
61
+ # attribute :description
62
+ # ...
63
+ # end
64
+ #
65
+ # There is now an instance method on Repo that will return that instances
66
+ # associated GithubRepo instance.
67
+ #
68
+ # repo = Repo.find(1)
69
+ # repo.github_repo #=> <class GithubRepo instance >
70
+ # repo.github_repo.description #=> "A ruby gem for ruby gems."
71
+ #
72
+ # as option:
73
+ #
74
+ # has_remote :github_repo, as: :on_github
75
+ #
76
+ # Specifying the 'as' option will override the created association method
77
+ # name. The method will now be 'on_github' rather than 'github_user'.
78
+ #
79
+ # repo = Repo.find(1)
80
+ # repo.on_github #=> <class GithubRepo instance >
81
+ # repo.on_github.description #=> "A ruby gem for ruby gems."
82
+ #
83
+ # scope option:
84
+ #
85
+ # has_remote :github_user, scope: { id: :gh_user_id }
86
+ #
87
+ # The scope hash in this example will evaluate the 'gh_user_id' method on
88
+ # this class and send it in as the :id on the scope Hash. This style scope
89
+ # argument is especially useful when the same remote resource class is
90
+ # mixed into multiple models, and therefore the multiple models are not
91
+ # forced to use the same method names for the scope values. The scope hash
92
+ # that is passed into the client and resource blocks will look like this:
93
+ # { id: 2345987 }
94
+ #
95
+ # has_remote :github_user, scope: [:id, :access_token]
96
+ #
97
+ # This scope array will evaluate both the :id and :access_token methods on
98
+ # this class. The scope hash that is passed into the client and resource
99
+ # blocks will look like this: { id: 274346, access_token: '2ac4g3t7jf8'}
100
+ #
101
+ # has_remote :github_user, scope: :github_login
102
+ #
103
+ # This will evaluate the :github_login method on this class. The scope
104
+ # hash that is passed into the client and resource blocks will look like
105
+ # this: { github_login: 'mkcode' }
106
+ #
107
+ # prefix and attribute_map options:
108
+ #
109
+ # has_remote :github_user, prefix: :gh_user
110
+ #
111
+ # All of the attribute methods defined on the association will be prefixed
112
+ # with 'gh_user_'. If the Base attributes class defined attributes :login
113
+ # and :email, this class would have the methods: :gh_user_login and
114
+ # :gh_user_email.
115
+ #
116
+ # has_remote :github_user, attributes_map: { email: :gh_email }
117
+ #
118
+ # The attributes_map argument here will override the name of the :email
119
+ # attribute. Instead of the method being named :email, it will now be
120
+ # named :gh_email.
121
+ #
122
+ # Returns an instance of AssociationBuilder.
123
+ def has_remote(which_klass, options = {})
124
+ klass = RemoteResource::Base.find_descendant(which_klass)
125
+ fail BaseClassNotFound.new(which_klass) unless klass
126
+
127
+ builder = AssociationBuilder.new(klass, options)
128
+ builder.associated_with(self)
129
+ end
130
+
131
+ # Public: embed_remote takes takes the exact same options as 'has_remote'
132
+ # and completes the same behavior as well with one addition. It defines the
133
+ # attribute getter methods directly on the target class, in addition to on
134
+ # the association. This is similar in theory to the object oriented
135
+ # programming 'is_a' vs 'has_a', inheritance vs composition debate. By
136
+ # defining the attribute methods directly on the domain object, that domain
137
+ # object 'is' a remote resource. Note that this will only define the
138
+ # attribute getter methods on the target class. Additional methods defined
139
+ # on the Resource class will not be copied, but may still be accessed
140
+ # through the association method.
141
+ #
142
+ # Examples
143
+ #
144
+ # class Repo < ActiveRecord::Base
145
+ # embed_remote :github_repo
146
+ #
147
+ # ...
148
+ # end
149
+ #
150
+ # class GithubRepo < HasRemote::Resource
151
+ # ...
152
+ # attribute :description
153
+ # ...
154
+ # end
155
+ #
156
+ # repo = Repo.find(1)
157
+ # repo.github_repo #=> <class GithubRepo instance >
158
+ # repo.github_repo.description #=> "A ruby gem for ruby gems."
159
+ #
160
+ # So far, this has been exactly the same as 'has_remote'. The addition:
161
+ #
162
+ # repo.description #=> "A ruby gem for ruby gems."
163
+ #
164
+ # Repo 'is' a GithubRepo.
165
+ #
166
+ # Returns nil
167
+ def embed_remote(which_klass, options = {})
168
+ assoc_builder = has_remote(which_klass, options)
169
+
170
+ attacher = AttributeMethodAttacher.new(assoc_builder.base_class, options)
171
+ attacher.attach_to(self, assoc_builder.options[:as].to_s)
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,24 @@
1
+ require 'logger'
2
+
3
+ module RemoteResource
4
+ module Configuration
5
+ # Our humble logger
6
+ module Logger
7
+ def self.extended(klass)
8
+ klass.instance_variable_set(:@logger, nil)
9
+ end
10
+
11
+ def logger=(logger)
12
+ @logger = logger
13
+ end
14
+
15
+ def logger
16
+ @logger ||= default_logger
17
+ end
18
+
19
+ def default_logger
20
+ ::Logger.new(STDOUT)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ require 'remote_resource/lookup/default'
2
+
3
+ module RemoteResource
4
+ module Configuration
5
+ # Our humble storage
6
+ module LookupMethod
7
+ def self.extended(klass)
8
+ klass.instance_variable_set(:@lookup_method, nil)
9
+ end
10
+
11
+ def lookup_method=(lookup_method)
12
+ @lookup_method = lookup_method
13
+ end
14
+
15
+ def lookup_method
16
+ @lookup_method ||= default_lookup_method
17
+ end
18
+
19
+ def default_lookup_method
20
+ Lookup::Default.new
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ require 'remote_resource/storage/memory'
2
+
3
+ module RemoteResource
4
+ module Configuration
5
+ # Our humble storage
6
+ module Storage
7
+ def self.extended(klass)
8
+ klass.instance_variable_set(:@storages, nil)
9
+ end
10
+
11
+ def storages=(storages)
12
+ @storages = storages
13
+ end
14
+
15
+ def storages
16
+ @storages ||= default_storages
17
+ end
18
+
19
+ def default_storages
20
+ [RemoteResource::Storage::Memory.new]
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,40 @@
1
+ require 'active_support/core_ext/string/strip'
2
+
3
+ module RemoteResource
4
+ class Error < StandardError; end
5
+
6
+ class ApiReadOnlyMethod < Error
7
+ def initialize(method_name)
8
+ @method_name = method_name
9
+ super(message)
10
+ end
11
+
12
+ def message
13
+ <<-MESSAGE.strip_heredoc
14
+
15
+ The `RemoteResource` gem creates read only methods which represent
16
+ API values. `#{@method_name}` was defined using this gem and this error
17
+ is raised to indicate that these attributes are read only, although you
18
+ may override this behavior by defining a `#{@method_name}=` setter
19
+ method on this class.
20
+ MESSAGE
21
+ end
22
+ end
23
+
24
+ class BaseClassNotFound < Error
25
+ def initialize(which_klass)
26
+ @which_klass = which_klass
27
+ super(message)
28
+ end
29
+
30
+ def message
31
+ <<-MESSAGE.strip_heredoc
32
+
33
+ A RemoteResource::Base class descendant named `#{@which_klass}`
34
+ could not be found. Descendant class names are generally suffixed with
35
+ 'Attributes' and looked up without the attributes symbol. Example: A
36
+ base class named 'GithubUserAttributes' is looked up with :github_user.
37
+ MESSAGE
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,39 @@
1
+ require 'active_support/log_subscriber'
2
+
3
+ module RemoteResource
4
+ class LogSubscriber < ActiveSupport::LogSubscriber
5
+ def logger
6
+ RemoteResource.logger
7
+ end
8
+
9
+ def find(event)
10
+ log_action('Find', event)
11
+ end
12
+
13
+ def storage_lookup(event)
14
+ log_action('Storage lookup', event) { |payload| payload[:attribute] }
15
+ end
16
+
17
+ def http_get(event)
18
+ log_action('HTTP GET', event) { |payload| payload[:attribute] }
19
+ end
20
+
21
+ # the optional block acts as a filter for the log description. The block is
22
+ # passed the payload and its return value is used as the log description.
23
+ # The whole payload is used as the description if the block is omitted.
24
+ def log_action(action, event, &block)
25
+ payload = event.payload
26
+ description = block ? block.call(payload) : payload
27
+
28
+ if (attribute = payload[:attribute])
29
+ subject = attribute[:location]
30
+ action = "#{subject} #{action} (#{event.duration.round(2)}ms)"
31
+ end
32
+
33
+ action = color(action, GREEN, true)
34
+ debug("#{action} #{description}")
35
+ end
36
+ end
37
+ end
38
+
39
+ RemoteResource::LogSubscriber.attach_to :remote_resource
@@ -0,0 +1,39 @@
1
+ require 'active_support/core_ext/hash/reverse_merge'
2
+
3
+ require 'remote_resource/attribute_storage_value'
4
+
5
+ module RemoteResource
6
+ module Lookup
7
+ # Default lookup class. Top most level class used for looking up attributes
8
+ # across storages and remotely over http.
9
+ #
10
+ # Arguments:
11
+ # options:
12
+ # validate: 'cache_control', 'true', 'false' - Should values looked up
13
+ # in storage be validated with the server. The default value
14
+ # 'cache_control', sets this according to the server returned
15
+ # Cache-Control header. Values true and false override this.
16
+ class Default
17
+ def initialize(options = {})
18
+ @options = options.reverse_merge(validate: :cache_control)
19
+ end
20
+
21
+ def find(attribute)
22
+ store_value = AttributeStorageValue.new(attribute)
23
+ if store_value.data?
24
+ store_value.validate if should_validate?(store_value)
25
+ else
26
+ store_value.fetch
27
+ end
28
+ store_value
29
+ end
30
+
31
+ private
32
+
33
+ def should_validate?(store_value)
34
+ return @options[:validate] unless @options[:validate] == :cache_control
35
+ store_value.validateable? && store_value.expired?
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,17 @@
1
+ require 'active_support/notifications'
2
+
3
+ module RemoteResource
4
+ module Notifications
5
+ def instrument(*args, &block)
6
+ args[0] = args[0] + '.remote_resource' unless args[0].include?('.')
7
+ ActiveSupport::Notifications.instrument(*args, &block)
8
+ end
9
+
10
+ def instrument_attribute(*args, &block)
11
+ fail ArgumentError unless args[1].is_a? AttributeSpecification
12
+ args.push({}) unless args.last.is_a? Hash
13
+ args.last.merge!(attribute: args.delete_at(1).to_hash)
14
+ instrument(*args, &block)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ module RemoteResource
2
+ class Railtie < Rails::Railtie
3
+ ns = 'remote_resource'
4
+
5
+ console do
6
+ RemoteResource.logger = Rails.logger
7
+ end
8
+
9
+ initializer "#{ns}.logger", after: 'active_record.logger' do
10
+ RemoteResource.logger = Rails.logger
11
+ end
12
+
13
+ initializer "#{ns}.extend_active_record", after: 'active_record.set_configs' do
14
+ ActiveRecord::Base.send(:extend, RemoteResource::Bridge)
15
+ end
16
+
17
+ initializer "#{ns}.add_to_eager_load_paths" do |app|
18
+ app.config.paths.add 'app/remote_resources', eager_load: true
19
+ end
20
+ end
21
+ end