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,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