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,52 @@
1
+ module RemoteResource
2
+ class ScopeEvaluator
3
+ attr_reader :scope
4
+
5
+ def initialize(scope = nil)
6
+ @scope = normalize_scope(scope)
7
+ end
8
+
9
+ def evaluate_on(target_object)
10
+ scope = {}
11
+ @scope.each_pair do |attr_key, target_method|
12
+ scope[attr_key.to_sym] = target_object.send(target_method.to_sym)
13
+ end
14
+ scope
15
+ end
16
+
17
+ private
18
+ # Internal: Returns a Boolean indicating whether or not the scope should be
19
+ # looked up (evaluated on) the target_object. This allows direct values for
20
+ # the scope to be used, rather then references to methods on the target
21
+ # object.
22
+ def should_eval_attribute_scope?
23
+ @scope.values.all? { |scope_value| scope_value.is_a? Symbol }
24
+ end
25
+
26
+ # Internal: Returns a hash where the values of the scope have been evaluated
27
+ # on the provided target_object.
28
+ def eval_attribute_scope(target_object)
29
+ scope = {}
30
+ @scope.each_pair do |attr_key, target_method|
31
+ scope[attr_key.to_sym] = target_object.send(target_method.to_sym)
32
+ end
33
+ scope
34
+ end
35
+
36
+ # Internal: Normalizes the scope argument. Always returns the scope as a
37
+ # Hash, despite it being able to be specified as a Symbol, Array, or Hash.
38
+ # An undefined scope returns an empty Hash.
39
+ def normalize_scope(scope)
40
+ if ! scope
41
+ scope = {}
42
+ elsif scope.is_a? Symbol
43
+ scope = { scope => scope }
44
+ elsif scope.is_a? Array
45
+ scope = {}.tap do |hash|
46
+ scope.each { |method| hash[method.to_sym] = method.to_sym }
47
+ end
48
+ end
49
+ scope
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,120 @@
1
+ module RemoteResource
2
+ # A class to represent the 'Cache-Control' header options.
3
+ # > This implementation is based on 'rack-cache' internals by Ryan Tomayko.
4
+ # It breaks the several directives into keys/values and stores them into
5
+ # a Hash.
6
+ # This file stolen from faraday-http-cache @ 907c33ec25eba8317931e53c84cfa
7
+ # See: https://github.com/plataformatec/faraday-http-cache
8
+ class CacheControl
9
+ # Initialize a new CacheControl.
10
+ def initialize(header)
11
+ @directives = parse(header.to_s)
12
+ end
13
+
14
+ # Checks if the 'public' directive is present.
15
+ def public?
16
+ @directives['public']
17
+ end
18
+
19
+ # Checks if the 'private' directive is present.
20
+ def private?
21
+ @directives['private']
22
+ end
23
+
24
+ # Checks if the 'no-cache' directive is present.
25
+ def no_cache?
26
+ @directives['no-cache']
27
+ end
28
+
29
+ # Checks if the 'no-store' directive is present.
30
+ def no_store?
31
+ @directives['no-store']
32
+ end
33
+
34
+ # Gets the 'max-age' directive as an Integer.
35
+ #
36
+ # Returns nil if the 'max-age' directive isn't present.
37
+ def max_age
38
+ @directives['max-age'].to_i if @directives.key?('max-age')
39
+ end
40
+
41
+ # Gets the 'max-age' directive as an Integer.
42
+ #
43
+ # takes the age header integer value and reduces the max-age and s-maxage
44
+ # if present to account for having to remove static age header when caching responses
45
+ def normalize_max_ages(age)
46
+ if age > 0
47
+ @directives['max-age'] = @directives['max-age'].to_i - age if @directives.key?('max-age')
48
+ @directives['s-maxage'] = @directives['s-maxage'].to_i - age if @directives.key?('s-maxage')
49
+ end
50
+ end
51
+
52
+ # Gets the 's-maxage' directive as an Integer.
53
+ #
54
+ # Returns nil if the 's-maxage' directive isn't present.
55
+ def shared_max_age
56
+ @directives['s-maxage'].to_i if @directives.key?('s-maxage')
57
+ end
58
+ alias_method :s_maxage, :shared_max_age
59
+
60
+ # Internal: Checks if the 'must-revalidate' directive is present.
61
+ def must_revalidate?
62
+ @directives['must-revalidate']
63
+ end
64
+
65
+ # Internal: Checks if the 'proxy-revalidate' directive is present.
66
+ def proxy_revalidate?
67
+ @directives['proxy-revalidate']
68
+ end
69
+
70
+ # Gets the String representation for the cache directives.
71
+ # Directives are joined by a '=' and then combined into a single String
72
+ # separated by commas. Directives with a 'true' value will omit the '='
73
+ # sign and their value.
74
+ #
75
+ # Returns the Cache Control string.
76
+ def to_s
77
+ booleans = []
78
+ values = []
79
+
80
+ @directives.each do |key, value|
81
+ if value == true
82
+ booleans << key
83
+ elsif value
84
+ values << "#{key}=#{value}"
85
+ end
86
+ end
87
+
88
+ (booleans.sort + values.sort).join(', ')
89
+ end
90
+
91
+ private
92
+
93
+ # Parses the Cache Control string to a Hash.
94
+ # Existing whitespace will be removed and the string is split on commas.
95
+ # For each part everything before a '=' will be treated as the key
96
+ # and the exceeding will be treated as the value. If only the key is
97
+ # present then the assigned value will default to true.
98
+ #
99
+ # Examples:
100
+ # parse("max-age=600")
101
+ # # => { "max-age" => "600"}
102
+ #
103
+ # parse("max-age")
104
+ # # => { "max-age" => true }
105
+ #
106
+ # Returns a Hash.
107
+ def parse(header)
108
+ directives = {}
109
+
110
+ header.delete(' ').split(',').each do |part|
111
+ next if part.empty?
112
+
113
+ name, value = part.split('=', 2)
114
+ directives[name.downcase] = (value || true) unless name.empty?
115
+ end
116
+
117
+ directives
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,36 @@
1
+ require 'remote_resource/storage/serializers/marshal'
2
+
3
+ module RemoteResource
4
+ class DBCache
5
+ ADAPTERS = %i(active_record)
6
+
7
+ attr_reader :column_name
8
+ attr_accessor :target_instance
9
+
10
+ def initialize(_adapter, column_name, _serializer = :marshal)
11
+ @adapter = :active_record
12
+ @column_name = column_name.to_sym
13
+ @serializer = Serializers::MarshalSerializer.new
14
+ end
15
+
16
+ # always returns a hash
17
+ def read_column
18
+ raw = @target_instance.read_attribute(column_name)
19
+ raw ? @serializer.load(raw) : {}
20
+ end
21
+
22
+ def write_column(hash)
23
+ fail ArgumentError 'must be a hash!' unless hash.is_a? Hash
24
+ raw = @serializer.dump(hash)
25
+ @target_instance.update_attribute(column_name, raw)
26
+ end
27
+
28
+ def read_key(key)
29
+ read_column[key.to_sym]
30
+ end
31
+
32
+ def write_key(key, value)
33
+ write_column(read_column.merge(key.to_sym => value))
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,38 @@
1
+ require 'remote_resource/storage/db_cache'
2
+
3
+ module RemoteResource
4
+ class UnsupportedDatabase < StandardError; end
5
+
6
+ class DBCacheFactory
7
+ def initialize(base_class, options)
8
+ @base_class = base_class
9
+ @options = options
10
+ end
11
+
12
+ def create_for_class(target_class)
13
+ db_cache_adapter = db_cache_adapter_for(target_class)
14
+ if db_cache_adapter
15
+ column_name = @options[:cache_column] ||
16
+ default_or_magic_column_name_for(@base_class)
17
+ DBCache.new(db_cache_adapter, column_name)
18
+ else
19
+ fail UnsupportedDatabase
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def default_or_magic_column_name_for(base_class)
26
+ "#{base_class.underscore}_cache"
27
+ end
28
+
29
+ def db_cache_adapter_for(klass)
30
+ DBCache::ADAPTERS.detect do |adapter_name|
31
+ klass.ancestors.any? do |parent_class|
32
+ next unless (class_name = parent_class.name)
33
+ class_name.split('::').first.underscore.to_sym == adapter_name
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,27 @@
1
+ require 'remote_resource/storage/storage_entry'
2
+
3
+ module RemoteResource
4
+ module Storage
5
+ class Memory
6
+ attr_accessor :memory_value
7
+
8
+ def initialize
9
+ @memory_value = {}
10
+ end
11
+
12
+ def read_key(key)
13
+ value = @memory_value[key]
14
+ return nil unless value.is_a? Hash
15
+ StorageEntry.new(value[:headers], value[:data])
16
+ end
17
+
18
+ def write_key(key, storage_entry)
19
+ if @memory_value[key]
20
+ @memory_value[key].merge!(storage_entry.to_hash)
21
+ else
22
+ @memory_value[key] = storage_entry.to_hash
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,43 @@
1
+ require 'active_support/time'
2
+
3
+ require 'remote_resource/storage/cache_control'
4
+
5
+ module RemoteResource
6
+ # An unset storage entry
7
+ class NullStorageEntry
8
+ attr_reader :headers, :data
9
+
10
+ def initialize
11
+ @headers = {}
12
+ @data = {}
13
+ end
14
+
15
+ def to_hash
16
+ { data: @data, headers: @headers }
17
+ end
18
+
19
+ def cache_control
20
+ @cache_control ||= CacheControl.new('')
21
+ end
22
+
23
+ def expired?
24
+ true
25
+ end
26
+
27
+ def data?
28
+ false
29
+ end
30
+
31
+ def exists?
32
+ false
33
+ end
34
+
35
+ def headers_for_validation
36
+ {}
37
+ end
38
+
39
+ def validateable?
40
+ false
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,27 @@
1
+ require 'remote_resource/storage/serializers/marshal'
2
+ require 'remote_resource/storage/storage_entry'
3
+
4
+ module RemoteResource
5
+ module Storage
6
+ class Redis
7
+ def initialize(redis, serializer = nil)
8
+ @redis = redis
9
+ @serializer = serializer || Serializers::MarshalSerializer.new
10
+ end
11
+
12
+ def read_key(key)
13
+ redis_value = @redis.hgetall key
14
+ StorageEntry.new @serializer.load(redis_value['headers']),
15
+ @serializer.load(redis_value['data'])
16
+ end
17
+
18
+ def write_key(storage_key, storage_entry)
19
+ write_args = []
20
+ storage_entry.to_hash.each_pair do |key, value|
21
+ write_args.concat([key, @serializer.dump(value)]) unless value.empty?
22
+ end
23
+ @redis.hmset storage_key, *write_args
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,15 @@
1
+ module RemoteResource
2
+ module Storage
3
+ class UnImplemntedError < StandardError; end
4
+
5
+ class Serializer
6
+ def load(_object)
7
+ fail UnImplemntedError 'Must implement serializer#load'
8
+ end
9
+
10
+ def dump(_object)
11
+ fail UnImplemntedError 'Must implement serializer#dump'
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ require_relative '../serializer'
2
+
3
+ module RemoteResource
4
+ module Storage
5
+ module Serializers
6
+ class MarshalSerializer < Serializer
7
+ def load(object)
8
+ return nil unless object
9
+ Marshal.load(object)
10
+ end
11
+
12
+ def dump(object)
13
+ Marshal.dump(object)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,69 @@
1
+ require 'active_support/time'
2
+
3
+ require 'remote_resource/storage/cache_control'
4
+
5
+ module RemoteResource
6
+ # A storage entry closely resembles a network response. This seeks to
7
+ # counteract the impedance mismatch because API responses are done on the
8
+ # resource level and we want to query storages at the attribute level. Headers
9
+ # are also handled on the resource (response) level as well, and thus apply
10
+ # for many attributes.
11
+ class StorageEntry
12
+ def self.from_response(response)
13
+ new(response.headers, response.data)
14
+ end
15
+
16
+ attr_reader :headers, :data
17
+
18
+ def initialize(headers, data)
19
+ @headers = headers.try(:to_hash) || {}
20
+ @data = data.try(:to_hash) || {}
21
+ end
22
+
23
+ def to_hash
24
+ {}.tap do |hash|
25
+ hash[:data] = @data unless @data.size == 0
26
+ hash[:headers] = @headers unless @headers.size == 0
27
+ end
28
+ end
29
+
30
+ def cache_control
31
+ @cache_control ||= CacheControl.new(headers['cache-control'])
32
+ end
33
+
34
+ # TODO: Extract this and make it better
35
+ # See: https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.4
36
+ # Cache-Control (http 1.1) overrides Expires header (http: 1.0)
37
+ def expired?
38
+ if cache_control.must_revalidate?
39
+ true
40
+ elsif cache_control.max_age
41
+ expire = DateTime.parse(headers['date']) + cache_control.max_age.seconds
42
+ Time.now > expire
43
+ else
44
+ false
45
+ end
46
+ end
47
+
48
+ def data?
49
+ !data.empty?
50
+ end
51
+
52
+ def exists?
53
+ !headers.empty? || data?
54
+ end
55
+
56
+ def headers_for_validation
57
+ v_headers = {}
58
+ v_headers['If-None-Match'] = headers['etag'] if headers['etag']
59
+ if headers['last-modified']
60
+ v_headers['If-Modified-Since'] = headers['last-modified']
61
+ end
62
+ v_headers
63
+ end
64
+
65
+ def validateable?
66
+ headers.key?('last-modified') || headers.key?('etag')
67
+ end
68
+ end
69
+ end