pdc 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.editorconfig +27 -0
- data/.gitignore +10 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +21 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +32 -0
- data/Guardfile +36 -0
- data/LICENSE +21 -0
- data/LICENSE.txt +21 -0
- data/README.md +41 -0
- data/Rakefile +11 -0
- data/bin/console +11 -0
- data/bin/setup +11 -0
- data/docs/.gitignore +3 -0
- data/docs/LICENSE_sphinx_deployment +27 -0
- data/docs/Makefile +179 -0
- data/docs/source/conf.py +257 -0
- data/docs/source/example.rst +10 -0
- data/docs/source/index.rst +23 -0
- data/docs/sphinx_deployment.mk +117 -0
- data/examples/active_attr.rb +33 -0
- data/examples/http_failures.rb +18 -0
- data/examples/local_pdc_dev.rb +15 -0
- data/examples/logger.rb +50 -0
- data/examples/pdc_curb_access_token.rb +36 -0
- data/examples/pdc_resource_tests.rb +173 -0
- data/examples/pdc_test_cache.rb +48 -0
- data/examples/prod_failures.rb +26 -0
- data/examples/prod_pdc.rb +14 -0
- data/lib/pdc/base.rb +14 -0
- data/lib/pdc/config.rb +157 -0
- data/lib/pdc/errors.rb +8 -0
- data/lib/pdc/http/errors.rb +43 -0
- data/lib/pdc/http/request/append_slash.rb +19 -0
- data/lib/pdc/http/request.rb +12 -0
- data/lib/pdc/http/response/pagination.rb +43 -0
- data/lib/pdc/http/response/parser.rb +62 -0
- data/lib/pdc/http/response/raise_error.rb +13 -0
- data/lib/pdc/http/response.rb +3 -0
- data/lib/pdc/http/result.rb +28 -0
- data/lib/pdc/http.rb +12 -0
- data/lib/pdc/logger.rb +19 -0
- data/lib/pdc/resource/attribute_modifier.rb +43 -0
- data/lib/pdc/resource/attribute_store.rb +22 -0
- data/lib/pdc/resource/attributes.rb +144 -0
- data/lib/pdc/resource/errors.rb +3 -0
- data/lib/pdc/resource/identity.rb +75 -0
- data/lib/pdc/resource/path.rb +63 -0
- data/lib/pdc/resource/per_thread_registry.rb +54 -0
- data/lib/pdc/resource/relation/finder.rb +24 -0
- data/lib/pdc/resource/relation/pagination.rb +33 -0
- data/lib/pdc/resource/relation/query.rb +14 -0
- data/lib/pdc/resource/relation.rb +81 -0
- data/lib/pdc/resource/rest_api.rb +34 -0
- data/lib/pdc/resource/scope_registry.rb +19 -0
- data/lib/pdc/resource/scopes.rb +29 -0
- data/lib/pdc/resource/value_parser.rb +31 -0
- data/lib/pdc/resource/wip.rb +0 -0
- data/lib/pdc/resource.rb +12 -0
- data/lib/pdc/v1/arch.rb +6 -0
- data/lib/pdc/v1/product.rb +5 -0
- data/lib/pdc/v1/release.rb +18 -0
- data/lib/pdc/v1/release_variant.rb +17 -0
- data/lib/pdc/v1.rb +8 -0
- data/lib/pdc/version.rb +3 -0
- data/lib/pdc.rb +25 -0
- data/pdc.gemspec +38 -0
- data/spec/fixtures/vcr/_page_count_returns_total_count.yml +141 -0
- data/spec/fixtures/vcr/brew_can_be_nil.yml +61 -0
- data/spec/fixtures/vcr/brew_may_be_present.yml +50 -0
- data/spec/fixtures/vcr/caches_multiple_response.yml +173 -0
- data/spec/fixtures/vcr/caches_response_with_a_query.yml +64 -0
- data/spec/fixtures/vcr/caches_response_with_multiple_query.yml +64 -0
- data/spec/fixtures/vcr/caches_response_without_query.yml +61 -0
- data/spec/fixtures/vcr/can_iterate_using_each.yml +187 -0
- data/spec/fixtures/vcr/fetches_variants_of_a_release.yml +663 -0
- data/spec/fixtures/vcr/must_return_number_of_resources.yml +61 -0
- data/spec/fixtures/vcr/preserves_the_filters.yml +49 -0
- data/spec/fixtures/vcr/returns_resources_on_that_page.yml +49 -0
- data/spec/fixtures/vcr/returns_the_total_count_and_not_items_in_page.yml +135 -0
- data/spec/fixtures/vcr/should_not_be_in_the_list_of_attributes.yml +95 -0
- data/spec/fixtures/vcr/works_with_where.yml +49 -0
- data/spec/pdc/config_spec.rb +115 -0
- data/spec/pdc/http/errors_spec.rb +58 -0
- data/spec/pdc/resource/attributes_spec.rb +231 -0
- data/spec/pdc/resource/cache_spec.rb +88 -0
- data/spec/pdc/resource/count_spec.rb +45 -0
- data/spec/pdc/resource/identity_spec.rb +96 -0
- data/spec/pdc/resource/pagination_spec.rb +51 -0
- data/spec/pdc/resource/path_spec.rb +30 -0
- data/spec/pdc/resource/relation_spec.rb +94 -0
- data/spec/pdc/resource/rest_api_spec.rb +23 -0
- data/spec/pdc/resource/value_parser_spec.rb +15 -0
- data/spec/pdc/resource/wip_spec.rb +0 -0
- data/spec/pdc/v1/arch_spec.rb +34 -0
- data/spec/pdc/v1/release_spec.rb +54 -0
- data/spec/pdc/version_spec.rb +5 -0
- data/spec/spec_helper.rb +39 -0
- data/spec/support/fixtures.rb +116 -0
- data/spec/support/vcr.rb +10 -0
- data/spec/support/webmock.rb +34 -0
- metadata +295 -0
data/lib/pdc/logger.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module PDC
|
4
|
+
class << self
|
5
|
+
attr_writer :logger
|
6
|
+
|
7
|
+
def logger
|
8
|
+
@logger ||= ::Logger.new($stdout).tap do |log|
|
9
|
+
log.progname = name
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
module Logging
|
15
|
+
def logger
|
16
|
+
PDC.logger
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
|
3
|
+
module PDC::Resource
|
4
|
+
module AttributeModifier
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
# rename an existing attribute `from` to `to`
|
9
|
+
# NOTE from must exist and to must not
|
10
|
+
def attribute_rename(from, to)
|
11
|
+
attribute_modifications << [:rename, from, to]
|
12
|
+
end
|
13
|
+
|
14
|
+
def attribute_modifications
|
15
|
+
@attribute_modifications ||= []
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize(attrs = {})
|
20
|
+
super
|
21
|
+
self.class.attribute_modifications.each do |what, *args|
|
22
|
+
case what
|
23
|
+
when :rename
|
24
|
+
apply_attr_rename(*args)
|
25
|
+
else
|
26
|
+
PDC.logger.warn "Invalid attribute transformation #{what}: #{from} #{to}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
yield self if block_given?
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def apply_attr_rename(from, to)
|
36
|
+
if attributes.key?(from) && !attributes.key?(to)
|
37
|
+
attributes[to] = attributes.delete(from)
|
38
|
+
else
|
39
|
+
PDC.logger.info "rename: not applied for from: #{from}, to: #{to} | #{attributes}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module PDC::Resource
|
2
|
+
class AttributeStore < HashWithIndifferentAccess
|
3
|
+
def to_params
|
4
|
+
each_with_object({}) do |(key, value), parameters|
|
5
|
+
parameters[key] = parse_value(value)
|
6
|
+
end.with_indifferent_access
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def parse_value(value)
|
12
|
+
|
13
|
+
case
|
14
|
+
when value.is_a?(PDC::Base) then value.attributes.to_params
|
15
|
+
when value.is_a?(Array) then value.map { |v| parse_value(v) }
|
16
|
+
else value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,144 @@
|
|
1
|
+
module PDC::Resource
|
2
|
+
module Attributes
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
attr_reader :attributes
|
7
|
+
delegate :[], to: :attributes
|
8
|
+
end
|
9
|
+
|
10
|
+
# :nodoc:
|
11
|
+
module ClassMethods
|
12
|
+
# define attributes on Model using
|
13
|
+
# attributes :attribute_name, attribute_name_2 ...
|
14
|
+
#
|
15
|
+
def attributes(*names)
|
16
|
+
return attributes_metadata.keys if names.empty?
|
17
|
+
names.each { |n| attributes_metadata[n] ||= default_metadata }
|
18
|
+
define_methods_in_container(names)
|
19
|
+
end
|
20
|
+
|
21
|
+
def attribute(name, metadata)
|
22
|
+
attr_exists = attributes_metadata.key?(name)
|
23
|
+
attributes_metadata[name] ||= default_metadata
|
24
|
+
attributes_metadata[name].merge!(metadata)
|
25
|
+
|
26
|
+
define_methods_in_container(name) unless attr_exists
|
27
|
+
end
|
28
|
+
|
29
|
+
def attributes_metadata
|
30
|
+
@attributes_metadata ||= HashWithIndifferentAccess.new(primary_key => default_metadata )
|
31
|
+
end
|
32
|
+
|
33
|
+
def attribute_parser(name)
|
34
|
+
# do not add to attributes of the class if not already present
|
35
|
+
metadata = attributes_metadata.fetch(name, default_metadata)
|
36
|
+
metadata[:parser]
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def default_metadata
|
42
|
+
{ parser: ValueParser }
|
43
|
+
end
|
44
|
+
|
45
|
+
def define_methods_in_container(names)
|
46
|
+
instance_method_container.module_eval do
|
47
|
+
Array.wrap(names).each do |name|
|
48
|
+
define_method(name) do
|
49
|
+
attribute(name)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# By adding instance methods via an included module, they become
|
56
|
+
# overridable with "super".
|
57
|
+
# see: http://thepugautomatic.com/2013/07/dsom/
|
58
|
+
def instance_method_container
|
59
|
+
unless @instance_method_container
|
60
|
+
@instance_method_container = Module.new
|
61
|
+
include @instance_method_container
|
62
|
+
end
|
63
|
+
@instance_method_container
|
64
|
+
end
|
65
|
+
end # class methods
|
66
|
+
|
67
|
+
def initialize(attributes = {})
|
68
|
+
self.attributes = attributes
|
69
|
+
@uri_template = scoped.uri
|
70
|
+
yield(self) if block_given?
|
71
|
+
end
|
72
|
+
|
73
|
+
def attributes=(new_attributes)
|
74
|
+
@attributes ||= AttributeStore.new
|
75
|
+
use_setters(new_attributes) if new_attributes
|
76
|
+
end
|
77
|
+
|
78
|
+
def []=(name, value)
|
79
|
+
set_attribute(name, value)
|
80
|
+
end
|
81
|
+
|
82
|
+
def inspect
|
83
|
+
"#<#{self.class}(#{@uri_template}) id: #{id.inspect} #{inspect_attributes}>"
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def use_setters(attributes)
|
89
|
+
attributes.each { |key, value| public_send "#{key}=", value }
|
90
|
+
end
|
91
|
+
|
92
|
+
def method_missing(name, *args, &block)
|
93
|
+
case
|
94
|
+
when attribute?(name) then attribute(name)
|
95
|
+
when predicate?(name) then predicate(name)
|
96
|
+
when setter?(name) then set_attribute(name, args.first)
|
97
|
+
else super
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def respond_to_missing?(name, include_private = false)
|
102
|
+
attribute?(name) || predicate?(name) || setter?(name) || super
|
103
|
+
end
|
104
|
+
|
105
|
+
def attribute?(name)
|
106
|
+
attributes.key?(name) || self.class.attributes_metadata.key?(name)
|
107
|
+
end
|
108
|
+
|
109
|
+
def attribute(name)
|
110
|
+
attributes[name]
|
111
|
+
end
|
112
|
+
|
113
|
+
def predicate?(name)
|
114
|
+
name.to_s.end_with?('?')
|
115
|
+
end
|
116
|
+
|
117
|
+
def predicate(name)
|
118
|
+
attribute(depredicate(name)).present?
|
119
|
+
end
|
120
|
+
|
121
|
+
def depredicate(name)
|
122
|
+
name.to_s.chomp('?').to_sym
|
123
|
+
end
|
124
|
+
|
125
|
+
def setter?(name)
|
126
|
+
name.to_s.end_with?('=')
|
127
|
+
end
|
128
|
+
|
129
|
+
def set_attribute(name, value)
|
130
|
+
attr_name = name.to_s.chomp('=')
|
131
|
+
unless attribute?(attr_name)
|
132
|
+
logger.warn "Setting unknown attribute: #{attr_name} #{self.class.name}"
|
133
|
+
end
|
134
|
+
parser = self.class.attribute_parser(attr_name)
|
135
|
+
attributes[attr_name] = parser.parse(value)
|
136
|
+
end
|
137
|
+
|
138
|
+
def inspect_attributes
|
139
|
+
attributes.map { |k, v| "#{k}: #{v.inspect}" }.join(' ')
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'pdc'
|
2
|
+
require_relative 'path'
|
3
|
+
|
4
|
+
module PDC::Resource
|
5
|
+
# handles id, primary_key and uri of a Resource
|
6
|
+
module Identity
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def primary_key
|
11
|
+
@primary_key ||= default_primary_key
|
12
|
+
end
|
13
|
+
|
14
|
+
def primary_key=(value)
|
15
|
+
@primary_key = value
|
16
|
+
end
|
17
|
+
|
18
|
+
def uri(value = nil)
|
19
|
+
@uri ||= Path.new(value || default_uri).to_s
|
20
|
+
end
|
21
|
+
|
22
|
+
# returns <version>/<resource-name> from the class.name
|
23
|
+
def resource_path
|
24
|
+
@resource_path ||= model_name.collection.sub(%r{^pdc\/}, '').tr('_', '-')
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def default_uri
|
30
|
+
resource_path + "/(:#{primary_key})"
|
31
|
+
end
|
32
|
+
|
33
|
+
# default pkey for FooBar is foo_bar_id
|
34
|
+
def default_primary_key
|
35
|
+
model_name.foreign_key.to_s
|
36
|
+
end
|
37
|
+
end # ClassMethods
|
38
|
+
|
39
|
+
def id?
|
40
|
+
attributes[primary_key].present?
|
41
|
+
end
|
42
|
+
|
43
|
+
def id
|
44
|
+
attributes[primary_key]
|
45
|
+
end
|
46
|
+
|
47
|
+
def id=(value)
|
48
|
+
attributes[primary_key] = value if value.present?
|
49
|
+
end
|
50
|
+
|
51
|
+
def hash
|
52
|
+
id.hash
|
53
|
+
end
|
54
|
+
|
55
|
+
def ==(other)
|
56
|
+
other.instance_of?(self.class) && id? && id == other.id
|
57
|
+
end
|
58
|
+
alias :eql? :==
|
59
|
+
|
60
|
+
def as_json(options = nil)
|
61
|
+
attributes.as_json(options)
|
62
|
+
end
|
63
|
+
|
64
|
+
def uri
|
65
|
+
Path.new(self.class.uri.to_s, attributes).expanded
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
# helper method so that primary_key can be called directly
|
70
|
+
# from an instance
|
71
|
+
def primary_key
|
72
|
+
self.class.primary_key
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'uri_template'
|
2
|
+
|
3
|
+
module PDC::Resource
|
4
|
+
class Path
|
5
|
+
|
6
|
+
def initialize(pattern, params = {})
|
7
|
+
@pattern = pattern
|
8
|
+
@params = params.symbolize_keys
|
9
|
+
end
|
10
|
+
|
11
|
+
def join(other_path)
|
12
|
+
self.class.new File.join(path, other_path.to_s), @params
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
@pattern
|
17
|
+
end
|
18
|
+
|
19
|
+
def expanded
|
20
|
+
path.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
def variables
|
24
|
+
@variables ||= uri_template.variables.map(&:to_sym)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def path
|
30
|
+
validate_required_params!
|
31
|
+
uri_template.expand(@params).chomp('/')
|
32
|
+
end
|
33
|
+
|
34
|
+
def uri_template
|
35
|
+
@uri_template ||= URITemplate.new(:colon, pattern_with_rfc_style_parens)
|
36
|
+
end
|
37
|
+
|
38
|
+
def pattern_with_rfc_style_parens
|
39
|
+
@pattern.gsub('(', '{').gsub(')', '}')
|
40
|
+
end
|
41
|
+
|
42
|
+
def validate_required_params!
|
43
|
+
return unless missing_required_params.any?
|
44
|
+
missing_params = missing_required_params.join(', ')
|
45
|
+
raise PDC::InvalidPathError, "Missing required params: #{missing_params} in #{@pattern}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def missing_required_params
|
49
|
+
required_params - params_with_values
|
50
|
+
end
|
51
|
+
|
52
|
+
def params_with_values
|
53
|
+
@params.map do |key, value|
|
54
|
+
key if value.present?
|
55
|
+
end.compact
|
56
|
+
end
|
57
|
+
|
58
|
+
def required_params
|
59
|
+
@pattern.scan(/\/:(\w+)/).flatten.map(&:to_sym)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# PerThreadRegistry from ActiveSupport 4.0
|
2
|
+
|
3
|
+
# This module is used to encapsulate access to thread local variables.
|
4
|
+
#
|
5
|
+
# Instead of polluting the thread locals namespace:
|
6
|
+
#
|
7
|
+
# Thread.current[:connection_handler]
|
8
|
+
#
|
9
|
+
# you define a class that extends this module:
|
10
|
+
#
|
11
|
+
# module ActiveRecord
|
12
|
+
# class RuntimeRegistry
|
13
|
+
# extend ActiveSupport::PerThreadRegistry
|
14
|
+
#
|
15
|
+
# attr_accessor :connection_handler
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# and invoke the declared instance accessors as class methods. So
|
20
|
+
#
|
21
|
+
# ActiveRecord::RuntimeRegistry.connection_handler = connection_handler
|
22
|
+
#
|
23
|
+
# sets a connection handler local to the current thread, and
|
24
|
+
#
|
25
|
+
# ActiveRecord::RuntimeRegistry.connection_handler
|
26
|
+
#
|
27
|
+
# returns a connection handler local to the current thread.
|
28
|
+
#
|
29
|
+
# This feature is accomplished by instantiating the class and storing the
|
30
|
+
# instance as a thread local keyed by the class name. In the example above
|
31
|
+
# a key "ActiveRecord::RuntimeRegistry" is stored in <tt>Thread.current</tt>.
|
32
|
+
# The class methods proxy to said thread local instance.
|
33
|
+
#
|
34
|
+
# If the class has an initializer, it must accept no arguments.
|
35
|
+
|
36
|
+
module PDC::Resource
|
37
|
+
module PerThreadRegistry
|
38
|
+
def self.extended(object)
|
39
|
+
object.instance_variable_set '@per_thread_registry_key', object.name.freeze
|
40
|
+
end
|
41
|
+
def instance
|
42
|
+
Thread.current[@per_thread_registry_key] ||= new
|
43
|
+
end
|
44
|
+
protected
|
45
|
+
def method_missing(name, *args, &block) # :nodoc:
|
46
|
+
# Caches the method definition as a singleton method of the receiver.
|
47
|
+
#
|
48
|
+
# By letting #delegate handle it, we avoid an enclosure that'll capture args.
|
49
|
+
singleton_class.delegate name, to: :instance
|
50
|
+
send(name, *args, &block)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module PDC::Resource
|
2
|
+
module Finder
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
# returns the contents for the current scope without any pagination as
|
6
|
+
# an +Array+ of +Resources+
|
7
|
+
def contents!
|
8
|
+
return @contents if @contents
|
9
|
+
@contents = result.data.map { |result| new(result) }
|
10
|
+
end
|
11
|
+
|
12
|
+
def find_one!
|
13
|
+
raise(PDC::ResourceNotFound, params) if result.data.empty?
|
14
|
+
raise(PDC::MultipleResultsError, params) if result.data.length > 1
|
15
|
+
@find_one ||= new(result.data.first)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def result
|
20
|
+
@result ||= fetch(clone)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module PDC::Resource
|
2
|
+
PAGINATION = :pagination
|
3
|
+
PAGINATION_KEYS = [
|
4
|
+
:resource_count,
|
5
|
+
:previous_page,
|
6
|
+
:next_page,
|
7
|
+
].freeze
|
8
|
+
|
9
|
+
module Pagination
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
PDC::Resource::PAGINATION_KEYS.each do |symbol|
|
13
|
+
define_method(symbol) do
|
14
|
+
result.pagination[symbol]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def each_page
|
19
|
+
return to_enum(:each_page) unless block_given?
|
20
|
+
|
21
|
+
# results are not fetched yet so use the clone for next pages
|
22
|
+
# and create new relation based on the next_page metadata
|
23
|
+
|
24
|
+
relation = clone
|
25
|
+
yield relation
|
26
|
+
|
27
|
+
until (next_page = relation.next_page).nil?
|
28
|
+
relation = self.class.new(klass, uri: next_page)
|
29
|
+
yield relation
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'pdc/resource/relation/query'
|
2
|
+
require 'pdc/resource/relation/pagination'
|
3
|
+
require 'pdc/resource/relation/finder'
|
4
|
+
|
5
|
+
module PDC::Resource
|
6
|
+
class Relation
|
7
|
+
include Enumerable
|
8
|
+
include PDC::Logging
|
9
|
+
include Query
|
10
|
+
include Finder
|
11
|
+
include Pagination
|
12
|
+
|
13
|
+
attr_reader :klass
|
14
|
+
attr_writer :params
|
15
|
+
|
16
|
+
alias :all :to_a
|
17
|
+
|
18
|
+
def initialize(klass, options = {})
|
19
|
+
@klass = klass
|
20
|
+
@options = options
|
21
|
+
@params = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
def params
|
25
|
+
@params.symbolize_keys
|
26
|
+
end
|
27
|
+
|
28
|
+
def uri
|
29
|
+
@options[:uri] || klass.uri
|
30
|
+
end
|
31
|
+
|
32
|
+
# TODO: need to scale this so that mulitle variables in a uri can
|
33
|
+
# be passed - e.g.
|
34
|
+
# ReleaseVarant.uri is 'v1/release-variant/(:release)/(:id)'
|
35
|
+
#
|
36
|
+
# so find(id, release: 'rel') need to work
|
37
|
+
def find(id, vars = {})
|
38
|
+
raise PDC::ResourceNotFound if id.blank?
|
39
|
+
where(primary_key => id)
|
40
|
+
.where(vars)
|
41
|
+
.find_one!
|
42
|
+
end
|
43
|
+
|
44
|
+
# TODO: handle pagination here
|
45
|
+
def each(&block)
|
46
|
+
return to_enum(:each) unless block_given?
|
47
|
+
|
48
|
+
each_page do |relation|
|
49
|
+
resources = relation.contents!
|
50
|
+
resources.each(&block)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# all! returns all records of the +Resource+
|
55
|
+
# NOTE: use this method for resources with small footprint
|
56
|
+
def all!
|
57
|
+
where(page_size: -1).contents!
|
58
|
+
end
|
59
|
+
|
60
|
+
def count
|
61
|
+
result.pagination[:resource_count]
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def method_missing(name, *args, &block)
|
67
|
+
# pass anything that relation doesn't know to the klass
|
68
|
+
super unless klass.respond_to? name
|
69
|
+
|
70
|
+
with_scope { klass.send(name, *args, &block) }
|
71
|
+
end
|
72
|
+
|
73
|
+
# Keep hold of current scope while running a method on the class
|
74
|
+
def with_scope
|
75
|
+
previous, klass.current_scope = klass.current_scope, self
|
76
|
+
yield
|
77
|
+
ensure
|
78
|
+
klass.current_scope = previous
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module PDC::Resource
|
2
|
+
module RestApi
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
class_attribute :connection, instance_accessor: false
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def request(method, path, query = {})
|
11
|
+
PDC.logger.debug ' >>>'.yellow + " : #{path.ai} #{query.ai}"
|
12
|
+
ActiveSupport::Notifications.instrument('request.pdc', method: method) do |payload|
|
13
|
+
response = connection.send(method) do |request|
|
14
|
+
request.url path, query
|
15
|
+
end
|
16
|
+
payload[:url], payload[:status] = response.env.url, response.status
|
17
|
+
PDC::Http::Result.new(response)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def fetch(relation)
|
22
|
+
# TODO: handle v1/release-variant/(:release)/(:variant)
|
23
|
+
# attributes won't contain (:release) and (:variant_id)
|
24
|
+
params = relation.params
|
25
|
+
resource_path = Path.new(relation.uri, params)
|
26
|
+
|
27
|
+
uri = resource_path.expanded
|
28
|
+
query = params.except(*resource_path.variables)
|
29
|
+
request(:get, uri, query)
|
30
|
+
end
|
31
|
+
end # classmethod
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require_relative 'per_thread_registry'
|
2
|
+
|
3
|
+
module PDC::Resource
|
4
|
+
class ScopeRegistry
|
5
|
+
extend PerThreadRegistry
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@store = Hash.new { |hash, key| hash[key] = {} }
|
9
|
+
end
|
10
|
+
|
11
|
+
def value_for(scope_type, variable_name)
|
12
|
+
@store[scope_type][variable_name]
|
13
|
+
end
|
14
|
+
|
15
|
+
def set_value_for(scope_type, variable_name, value)
|
16
|
+
@store[scope_type][variable_name] = value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module PDC::Resource
|
2
|
+
module Scopes
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
module ClassMethods
|
6
|
+
delegate :find, :count, :where, :first, :all!, :all, to: :scoped
|
7
|
+
|
8
|
+
def scoped
|
9
|
+
current_scope || Relation.new(self, uri: uri)
|
10
|
+
end
|
11
|
+
|
12
|
+
def scope(name, code)
|
13
|
+
define_singleton_method name, code
|
14
|
+
end
|
15
|
+
|
16
|
+
def current_scope=(value)
|
17
|
+
ScopeRegistry.set_value_for(:current_scope, name, value)
|
18
|
+
end
|
19
|
+
|
20
|
+
def current_scope
|
21
|
+
ScopeRegistry.value_for(:current_scope, name)
|
22
|
+
end
|
23
|
+
end # class methods
|
24
|
+
|
25
|
+
def scoped
|
26
|
+
self.class.scoped
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module PDC::Resource
|
2
|
+
# Internal
|
3
|
+
# ValueParser takes in a value and returns the parsed form of the input
|
4
|
+
# e.g. Hash gets converted to OpenStruct so that
|
5
|
+
# foo: {
|
6
|
+
# bar: {
|
7
|
+
# too: 'too moo',
|
8
|
+
# baz: {
|
9
|
+
# value: 'foobarbaz'
|
10
|
+
# }
|
11
|
+
# }
|
12
|
+
# }
|
13
|
+
# can be accessed as foo.bar.too and foo.bar.baz.value
|
14
|
+
class ValueParser
|
15
|
+
class << self
|
16
|
+
def parse(value)
|
17
|
+
case
|
18
|
+
when value.is_a?(Array) then value.map { |v| parse(v) }
|
19
|
+
when value.is_a?(Hash) then OpenStruct.new(parse_hash(value))
|
20
|
+
else value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def parse_hash(hash)
|
27
|
+
hash.map { |k, v| [k.to_sym, parse(v)] }.to_h
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
File without changes
|