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.
- checksums.yaml +7 -0
- data/.codeclimate.yml +19 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.rubocop.yml +1156 -0
- data/.travis.yml +17 -0
- data/Gemfile +15 -0
- data/Guardfile +17 -0
- data/LICENSE.txt +21 -0
- data/Procfile.dev +5 -0
- data/README.md +314 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/lib/remote_resource/association_builder.rb +47 -0
- data/lib/remote_resource/attribute_http_client.rb +41 -0
- data/lib/remote_resource/attribute_key.rb +26 -0
- data/lib/remote_resource/attribute_method_attacher.rb +117 -0
- data/lib/remote_resource/attribute_specification.rb +51 -0
- data/lib/remote_resource/attribute_storage_value.rb +63 -0
- data/lib/remote_resource/base/attributes.rb +44 -0
- data/lib/remote_resource/base/base_class_methods.rb +23 -0
- data/lib/remote_resource/base/dsl.rb +27 -0
- data/lib/remote_resource/base/rescue.rb +43 -0
- data/lib/remote_resource/base.rb +35 -0
- data/lib/remote_resource/bridge.rb +174 -0
- data/lib/remote_resource/configuration/logger.rb +24 -0
- data/lib/remote_resource/configuration/lookup_method.rb +24 -0
- data/lib/remote_resource/configuration/storage.rb +24 -0
- data/lib/remote_resource/errors.rb +40 -0
- data/lib/remote_resource/log_subscriber.rb +39 -0
- data/lib/remote_resource/lookup/default.rb +39 -0
- data/lib/remote_resource/notifications.rb +17 -0
- data/lib/remote_resource/railtie.rb +21 -0
- data/lib/remote_resource/scope_evaluator.rb +52 -0
- data/lib/remote_resource/storage/cache_control.rb +120 -0
- data/lib/remote_resource/storage/db_cache.rb +36 -0
- data/lib/remote_resource/storage/db_cache_factory.rb +38 -0
- data/lib/remote_resource/storage/memory.rb +27 -0
- data/lib/remote_resource/storage/null_storage_entry.rb +43 -0
- data/lib/remote_resource/storage/redis.rb +27 -0
- data/lib/remote_resource/storage/serializer.rb +15 -0
- data/lib/remote_resource/storage/serializers/marshal.rb +18 -0
- data/lib/remote_resource/storage/storage_entry.rb +69 -0
- data/lib/remote_resource/version.rb +3 -0
- data/lib/remote_resource.rb +34 -0
- data/remote-resource.gemspec +27 -0
- metadata +175 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'remote_resource/notifications'
|
2
|
+
|
3
|
+
module RemoteResource
|
4
|
+
class AttributeHttpClient
|
5
|
+
include RemoteResource::Notifications
|
6
|
+
|
7
|
+
def initialize(attribute, client = nil)
|
8
|
+
@attribute = attribute
|
9
|
+
@client = client || @attribute.client
|
10
|
+
@resource_name = @attribute.resource_name
|
11
|
+
end
|
12
|
+
|
13
|
+
def get(headers = {})
|
14
|
+
instrument_attribute('http_get', @attribute) do
|
15
|
+
if headers && headers.size > 0
|
16
|
+
with_headers_for_method(:get, headers) do |client|
|
17
|
+
@attribute.resource(client)
|
18
|
+
end
|
19
|
+
else
|
20
|
+
@attribute.resource(@client)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
@client.last_response
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# Internal: yield a client with headers bound on the supplied method.
|
29
|
+
def with_headers_for_method(method, headers)
|
30
|
+
old_method = "orig_#{method}".to_sym
|
31
|
+
client_class = @client.singleton_class
|
32
|
+
client_class.send(:alias_method, old_method, method)
|
33
|
+
client_class.send(:define_method, method) do |url, _|
|
34
|
+
send(old_method, url, headers: headers)
|
35
|
+
end
|
36
|
+
yield @client
|
37
|
+
client_class.send(:alias_method, method, old_method)
|
38
|
+
client_class.send(:remove_method, old_method)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module RemoteResource
|
2
|
+
class AttributeKey
|
3
|
+
attr_reader :prefix, :named_resource, :method
|
4
|
+
|
5
|
+
def initialize(prefix, named_resource, scope, method)
|
6
|
+
@prefix = prefix
|
7
|
+
@named_resource = named_resource
|
8
|
+
@scope = scope || {}
|
9
|
+
@method = method
|
10
|
+
end
|
11
|
+
|
12
|
+
def scope_string
|
13
|
+
@scope.map { |k, v| "#{k}=#{v}" }.join('&')
|
14
|
+
end
|
15
|
+
|
16
|
+
def for_resource
|
17
|
+
[@prefix, scope_string, @named_resource].compact.join('/')
|
18
|
+
end
|
19
|
+
alias_method :for_storage, :for_resource
|
20
|
+
|
21
|
+
def for_attribute
|
22
|
+
[@prefix, scope_string, @named_resource, @method].compact.join('/')
|
23
|
+
end
|
24
|
+
alias_method :to_s, :for_attribute
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module RemoteResource
|
2
|
+
# non-anonymous namespace for our generated methods. Mixed into the target
|
3
|
+
# class so that introspection shows where the methods came from.
|
4
|
+
class AttributeMethods < Module; end
|
5
|
+
|
6
|
+
# Public: Creates an instance of the AttributeMethodAttacher, which is
|
7
|
+
# responsible for setting up and attaching methods onto a target class which
|
8
|
+
# are used to lookup cached attributes.
|
9
|
+
class AttributeMethodAttacher
|
10
|
+
attr_accessor :options
|
11
|
+
# Public: Creates an instance of the AttributeMethodAttacher, which is
|
12
|
+
# responsible for setting up and attaching methods onto a target class which
|
13
|
+
# are used to lookup cached attributes.
|
14
|
+
#
|
15
|
+
# base_class - the class which will have methods attached to.
|
16
|
+
# options - options hash with the following keys (default: {}). Generally,
|
17
|
+
# prefix should be used when overriding the names of all the
|
18
|
+
# methods is desired and attributes_map should be used when
|
19
|
+
# overriding only a few method names is desired.
|
20
|
+
# :prefix - prefix for the names of the newly created methods.
|
21
|
+
# :attributes_map - a hash for overriding method names to create. The
|
22
|
+
# keys in this hash represent an attribute defined on
|
23
|
+
# the base_class. The values are the overriding name
|
24
|
+
# of the method to be defined on the target_class.
|
25
|
+
#
|
26
|
+
# Returns a new instance.
|
27
|
+
def initialize(base_class, options = {})
|
28
|
+
@base_class = base_class
|
29
|
+
@options = ensure_options(options)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Public: Set the base_classes' attribute methods on the given target class.
|
33
|
+
# Sets a MethodResolver instance on the target_class as well. Logs warnings
|
34
|
+
# if any methods on the target class are overwritten.
|
35
|
+
#
|
36
|
+
# target_class - The class upon which the base_classes' attribute methods
|
37
|
+
# should be set.
|
38
|
+
# path_to_attrs - A string representing a line of ruby code that will be
|
39
|
+
# evaluated. This line is the relative path from a
|
40
|
+
# target_class instance to the object that holds the
|
41
|
+
# attributes and where the `get_attribute` is defined. If
|
42
|
+
# path_to_attrs is (default: self), that implies that this
|
43
|
+
# is defining methods on the Base class, because that object
|
44
|
+
# contains the `get_attribute` method.
|
45
|
+
#
|
46
|
+
# Returns the target_class
|
47
|
+
def attach_to(target_class, path_to_attrs = 'self')
|
48
|
+
overwrite_method_warnings(target_class) unless path_to_attrs == 'self'
|
49
|
+
target_class.send(:include, make_attribute_methods_module(path_to_attrs))
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# Internal: Returns a module with the base_classes' attributes defined as
|
55
|
+
# getter and setter methods.
|
56
|
+
def make_attribute_methods_module(path_to_attrs)
|
57
|
+
attribute_methods_module = AttributeMethods.new
|
58
|
+
|
59
|
+
@base_class.attributes.keys.each do |attributes_method|
|
60
|
+
method_name = @options[:attributes_map][attributes_method]
|
61
|
+
method_name = "#{@options[:prefix]}_#{method_name}" if @options[:prefix]
|
62
|
+
attribute_methods_module.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
63
|
+
def #{method_name}
|
64
|
+
#{path_to_attrs}.get_attribute(:#{attributes_method})
|
65
|
+
end
|
66
|
+
|
67
|
+
def #{method_name}=(other)
|
68
|
+
fail ApiReadOnlyMethod.new('#{method_name}')
|
69
|
+
end
|
70
|
+
RUBY
|
71
|
+
end
|
72
|
+
attribute_methods_module
|
73
|
+
end
|
74
|
+
|
75
|
+
# Internal: Populate the attributes map. If no attributes_map arg is given,
|
76
|
+
# then attributes_map is a 1 to 1 map (the default_attributes_map below).
|
77
|
+
def ensure_options(options)
|
78
|
+
options[:attributes_map] = default_attributes_map
|
79
|
+
.merge(options[:attributes_map] || {})
|
80
|
+
options
|
81
|
+
end
|
82
|
+
|
83
|
+
# Internal: Create a basic 1 to 1 (key to value) Hash map of the attribute
|
84
|
+
# names. This is intended to be overridden by the attributes_map argument.
|
85
|
+
def default_attributes_map
|
86
|
+
{}.tap do |attr_map|
|
87
|
+
@base_class.attributes.keys.each do |method|
|
88
|
+
attr_map[method.to_sym] = method.to_sym
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Internal: Log warning methods when this attacher will overwrite methods on
|
94
|
+
# the target class.
|
95
|
+
def overwrite_method_warnings(target_class)
|
96
|
+
@options[:attributes_map].values.each do |method|
|
97
|
+
method = "#{@options[:prefix]}_#{method}" if @options[:prefix]
|
98
|
+
if present_and_future_public_methods(target_class).include? method
|
99
|
+
log_msg = "#{@base_class.name} is overwriting the "
|
100
|
+
log_msg += "#{method} method on #{target_class.name}"
|
101
|
+
RemoteResource.logger.warn log_msg
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Internal: ActiveRecord objects do not define a model's column methods
|
107
|
+
# until the instance is created. In this context, we have the class, so we
|
108
|
+
# lookup the 'future methods' on the `column_names` class method.
|
109
|
+
def present_and_future_public_methods(target_class)
|
110
|
+
conflicts = target_class.public_instance_methods
|
111
|
+
if target_class.respond_to? :column_names
|
112
|
+
conflicts += target_class.column_names.map(&:to_sym)
|
113
|
+
end
|
114
|
+
conflicts
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'remote_resource/attribute_key'
|
2
|
+
|
3
|
+
module RemoteResource
|
4
|
+
# A value object representing an attribute defined in
|
5
|
+
# RemoteResource::Base. An AttributeSpecification contains the
|
6
|
+
# attributes' method name, its resource, and its API client that is used for
|
7
|
+
# lookup. It also calculates its `key` which is used to lookup its value in
|
8
|
+
# storage.
|
9
|
+
#
|
10
|
+
# scope is evaluated outside of this object and should remain unchanged
|
11
|
+
# throughout its life cycle. scope is used in building the attribute's key and
|
12
|
+
# its equality. target_object on the other hand is just a reference to the
|
13
|
+
# object that scope was evaluated on. It may change throughout the attribute's
|
14
|
+
# life cycle and is not used in determining equality.
|
15
|
+
class AttributeSpecification
|
16
|
+
attr_reader :name, :base_class
|
17
|
+
delegate :client, :with_error_handling, to: :@base_class
|
18
|
+
|
19
|
+
def initialize(name, base_class)
|
20
|
+
@name = name
|
21
|
+
@base_class = base_class
|
22
|
+
end
|
23
|
+
alias_method :method, :name
|
24
|
+
|
25
|
+
def to_hash
|
26
|
+
{
|
27
|
+
name: @name,
|
28
|
+
resource: resource_name,
|
29
|
+
base_class: @base_class.class.symbol_name,
|
30
|
+
location: location
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def resource_name
|
35
|
+
@base_class.class.attributes[@name]
|
36
|
+
end
|
37
|
+
|
38
|
+
def resource(resource_client = nil)
|
39
|
+
@base_class.send(:resource, resource_name, resource_client)
|
40
|
+
end
|
41
|
+
|
42
|
+
def location
|
43
|
+
"#{@base_class.class.name}##{@name}"
|
44
|
+
end
|
45
|
+
|
46
|
+
def key
|
47
|
+
@key ||= AttributeKey.new(@base_class.class.underscore, resource_name,
|
48
|
+
@base_class.scope, @name)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'active_support/core_ext/module/delegation'
|
2
|
+
|
3
|
+
require 'remote_resource/attribute_http_client'
|
4
|
+
require 'remote_resource/storage/storage_entry'
|
5
|
+
require 'remote_resource/storage/null_storage_entry'
|
6
|
+
require 'remote_resource/notifications'
|
7
|
+
|
8
|
+
module RemoteResource
|
9
|
+
class AttributeStorageValue
|
10
|
+
include RemoteResource::Notifications
|
11
|
+
|
12
|
+
delegate :data?, :exists?, :expired?, :headers_for_validation,
|
13
|
+
:validateable?, to: :storage_entry
|
14
|
+
|
15
|
+
def initialize(attribute)
|
16
|
+
@attribute = attribute
|
17
|
+
@storage_entry = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def value
|
21
|
+
storage_entry.data[@attribute.name]
|
22
|
+
end
|
23
|
+
|
24
|
+
def storages
|
25
|
+
RemoteResource.storages
|
26
|
+
end
|
27
|
+
|
28
|
+
def fetch
|
29
|
+
@attribute.with_error_handling action: :fetch do
|
30
|
+
attr_client = AttributeHttpClient.new(@attribute)
|
31
|
+
write(StorageEntry.from_response(attr_client.get))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def validate
|
36
|
+
@attribute.with_error_handling action: :validate do
|
37
|
+
attr_client = AttributeHttpClient.new(@attribute)
|
38
|
+
response = attr_client.get(headers_for_validation)
|
39
|
+
write(StorageEntry.from_response(response))
|
40
|
+
response.headers['status'] == '304 Not Modified'
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def write(storage_entry)
|
45
|
+
@storage_entry = nil
|
46
|
+
storages.each do |storage|
|
47
|
+
storage.write_key(@attribute.key.for_storage, storage_entry)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def storage_entry
|
52
|
+
return @storage_entry if @storage_entry
|
53
|
+
instrument_attribute('storage_lookup', @attribute) do
|
54
|
+
storages.each do |storage|
|
55
|
+
if (storage_entry = storage.read_key(@attribute.key.for_storage))
|
56
|
+
return (@storage_entry = storage_entry)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
return (@storage_entry = NullStorageEntry.new)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module RemoteResource
|
2
|
+
# Defines attribute related methods for Base class instances.
|
3
|
+
module Attributes
|
4
|
+
include RemoteResource::Notifications
|
5
|
+
|
6
|
+
# Public: Creates and returns an array of cached attributes for the provided
|
7
|
+
# base_instance. The base_instance is stored in each attribute and is used
|
8
|
+
# for lookup.
|
9
|
+
#
|
10
|
+
# base_instance - an instance of base_class. The instance means that the
|
11
|
+
# scope has already been applied to it.
|
12
|
+
#
|
13
|
+
# Returns an array a AttributeSpecifications.
|
14
|
+
def create_attributes(base_instance)
|
15
|
+
@attributes = base_instance.class.attributes.map do |method, _value|
|
16
|
+
AttributeSpecification.new(method, base_instance)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Public: Returns the value of the provided attribute. It uses the
|
21
|
+
# application configured lookup_method to do so.
|
22
|
+
#
|
23
|
+
# name - a Symbol representing an attribute name
|
24
|
+
#
|
25
|
+
# Returns the value of the attribute as a String.
|
26
|
+
def get_attribute(name)
|
27
|
+
attribute = find_attribute(name)
|
28
|
+
|
29
|
+
attr_lookup = RemoteResource.lookup_method
|
30
|
+
lookup_name = attr_lookup.class.name
|
31
|
+
instrument_attribute('find', attribute, lookup_method: lookup_name) do
|
32
|
+
attr_lookup.find(attribute).value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# Internal: Returns the already created AttributeSpecification with the
|
39
|
+
# provided name.
|
40
|
+
def find_attribute(name)
|
41
|
+
@attributes.detect { |attr| attr.name == name }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module RemoteResource
|
2
|
+
# Methods that help with loading and naming the Base class.
|
3
|
+
module BaseClassMethods
|
4
|
+
def find_descendant(which_class)
|
5
|
+
ensure_loaded(which_class)
|
6
|
+
descendants.detect do |descendant|
|
7
|
+
descendant.symbol_name == which_class.to_sym
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def ensure_loaded(which_class)
|
12
|
+
which_class.to_s.camelize.safe_constantize
|
13
|
+
end
|
14
|
+
|
15
|
+
def underscore
|
16
|
+
name.underscore
|
17
|
+
end
|
18
|
+
|
19
|
+
def symbol_name
|
20
|
+
underscore.to_sym
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module RemoteResource
|
2
|
+
# Our humble DSL
|
3
|
+
module Dsl
|
4
|
+
attr_reader :client_proc, :resources
|
5
|
+
|
6
|
+
def client(&block)
|
7
|
+
@client_proc = block if block
|
8
|
+
end
|
9
|
+
|
10
|
+
def resource(name = :default, &block)
|
11
|
+
if block
|
12
|
+
@resources ||= {}
|
13
|
+
@resources[name] = block
|
14
|
+
else
|
15
|
+
fail ArgumentError, 'must supply a block'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def attribute(method, named_resource = :default)
|
20
|
+
attributes[method] = named_resource
|
21
|
+
end
|
22
|
+
|
23
|
+
def attributes
|
24
|
+
@attributes ||= {}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'active_support/rescuable'
|
3
|
+
|
4
|
+
module RemoteResource
|
5
|
+
# Methods that help with loading and naming the Base class.
|
6
|
+
module Rescue
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
include ActiveSupport::Rescuable
|
9
|
+
|
10
|
+
# Public: Use the with_error_handling method to use the error handling
|
11
|
+
# behavior defined in the rescue_from calls on the class. Take a block,
|
12
|
+
# whose body will error rescued.
|
13
|
+
#
|
14
|
+
# Note that it is typically 'wrong' in Ruby to rescue Exception. In this
|
15
|
+
# case it is OK because we re raise the error if it is not handled. Inspired
|
16
|
+
# by ActiveController.
|
17
|
+
# rubocop:disable Lint/RescueException
|
18
|
+
#
|
19
|
+
# Returns the last executed statement value.
|
20
|
+
def with_error_handling(context = {})
|
21
|
+
yield
|
22
|
+
rescue Exception => exception
|
23
|
+
rescue_with_handler(exception, context) || raise(exception)
|
24
|
+
end
|
25
|
+
# rubocop:enable Lint/RescueException
|
26
|
+
|
27
|
+
private
|
28
|
+
# Internal: Override the default ActiveSupport::Rescuable behavior to allow
|
29
|
+
# for additional context argument to be supplied as well. These error
|
30
|
+
# handlers are reused in different situations, and the context argument
|
31
|
+
# allows one to change the behavior depending on which situation.
|
32
|
+
def rescue_with_handler(exception, context = {})
|
33
|
+
if (handler = handler_for_rescue(exception))
|
34
|
+
case handler.arity
|
35
|
+
when 2 then handler.call(exception, context)
|
36
|
+
when 1 then handler.call(exception)
|
37
|
+
when 0 then handler.call
|
38
|
+
end
|
39
|
+
true # don't rely on the return value of the handler
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|