pdc 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/.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
|