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