reso_transport 1.5.2 → 1.5.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.gitpod.yml +2 -0
- data/.rubocop.yml +33 -0
- data/Gemfile.lock +1 -1
- data/README.md +96 -7
- data/bin/console +10 -10
- data/bin/rake +29 -0
- data/lib/reso_transport.rb +22 -35
- data/lib/reso_transport/authentication/fetch_token_auth.rb +39 -16
- data/lib/reso_transport/authentication/middleware.rb +5 -4
- data/lib/reso_transport/base_metadata.rb +64 -0
- data/lib/reso_transport/client.rb +33 -16
- data/lib/reso_transport/datasystem.rb +25 -0
- data/lib/reso_transport/datasystem_parser.rb +26 -0
- data/lib/reso_transport/entity_set.rb +2 -4
- data/lib/reso_transport/entity_type.rb +11 -13
- data/lib/reso_transport/errors.rb +69 -0
- data/lib/reso_transport/metadata.rb +15 -32
- data/lib/reso_transport/metadata_cache.rb +20 -0
- data/lib/reso_transport/metadata_parser.rb +31 -22
- data/lib/reso_transport/query.rb +44 -52
- data/lib/reso_transport/resource.rb +28 -8
- data/lib/reso_transport/version.rb +1 -1
- data/reso_transport.gemspec +20 -20
- metadata +22 -14
@@ -18,10 +18,11 @@ module ResoTransport
|
|
18
18
|
authorize_request(request_env)
|
19
19
|
|
20
20
|
@app.call(request_env).on_complete do |response_env|
|
21
|
-
raise_if_unauthorized(response_env)
|
21
|
+
raise_if_unauthorized(request_env, response_env)
|
22
22
|
end
|
23
23
|
rescue ResoTransport::AccessDenied
|
24
|
-
raise if retries
|
24
|
+
raise if retries.zero?
|
25
|
+
|
25
26
|
@auth.reset
|
26
27
|
retries -= 1
|
27
28
|
retry
|
@@ -38,8 +39,8 @@ module ResoTransport
|
|
38
39
|
)
|
39
40
|
end
|
40
41
|
|
41
|
-
def raise_if_unauthorized(response_env)
|
42
|
-
raise ResoTransport::AccessDenied if response_env[:status] == 401
|
42
|
+
def raise_if_unauthorized(request_env, response_env)
|
43
|
+
raise ResoTransport::AccessDenied.new(request_env.to_h, response_env) if response_env[:status] == 401
|
43
44
|
end
|
44
45
|
end
|
45
46
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module ResoTransport
|
2
|
+
class BaseMetadata
|
3
|
+
MIME_TYPES = {
|
4
|
+
xml: 'application/xml',
|
5
|
+
json: 'application/json'
|
6
|
+
}.freeze
|
7
|
+
|
8
|
+
attr_reader :client
|
9
|
+
|
10
|
+
def initialize(client)
|
11
|
+
@client = client
|
12
|
+
@prefix = nil
|
13
|
+
@classname = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def prefix
|
17
|
+
raise 'prefix not set' unless @prefix
|
18
|
+
|
19
|
+
@prefix
|
20
|
+
end
|
21
|
+
|
22
|
+
def classname
|
23
|
+
raise 'classname not set' unless @classname
|
24
|
+
|
25
|
+
@classname
|
26
|
+
end
|
27
|
+
|
28
|
+
def parser
|
29
|
+
@parser ||= Object::const_get("#{classname}Parser").new.parse(data)
|
30
|
+
end
|
31
|
+
|
32
|
+
def data
|
33
|
+
if cache_file
|
34
|
+
cache.read || cache.write(raw)
|
35
|
+
else
|
36
|
+
raw
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def cache
|
41
|
+
@cache ||= client.send("#{prefix}_cache").new(cache_file)
|
42
|
+
end
|
43
|
+
|
44
|
+
def cache_file
|
45
|
+
@cache_file ||= client.send "#{prefix}_file"
|
46
|
+
end
|
47
|
+
|
48
|
+
def raw
|
49
|
+
return response.body if response.success?
|
50
|
+
|
51
|
+
raise RequestError.new(request, response, classname)
|
52
|
+
end
|
53
|
+
|
54
|
+
def response
|
55
|
+
raise 'Must implement response method'
|
56
|
+
end
|
57
|
+
|
58
|
+
def request
|
59
|
+
return @request.to_h if @request.respond_to? :to_h
|
60
|
+
|
61
|
+
{}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -1,34 +1,52 @@
|
|
1
1
|
module ResoTransport
|
2
2
|
class Client
|
3
|
-
attr_reader :connection, :uid, :vendor, :endpoint, :
|
4
|
-
|
3
|
+
attr_reader :connection, :uid, :vendor, :endpoint, :authentication, :md_file, :md_cache, :ds_file, :ds_cache,
|
4
|
+
:use_replication_endpoint
|
5
|
+
|
5
6
|
def initialize(options)
|
6
|
-
@
|
7
|
-
@
|
8
|
-
@
|
9
|
-
@
|
10
|
-
@
|
11
|
-
@
|
12
|
-
|
13
|
-
@
|
7
|
+
@use_replication_endpoint = options.fetch(:use_replication_endpoint, false)
|
8
|
+
@endpoint = options.fetch(:endpoint)
|
9
|
+
@md_file = options.fetch(:md_file, nil)
|
10
|
+
@ds_file = options.fetch(:ds_file, nil)
|
11
|
+
@authentication = ensure_valid_auth_strategy(options.fetch(:authentication))
|
12
|
+
@vendor = options.fetch(:vendor, {})
|
13
|
+
@faraday_options = options.fetch(:faraday_options, {})
|
14
|
+
@logger = options.fetch(:logger, nil)
|
15
|
+
@md_cache = options.fetch(:md_cache, ResoTransport::MetadataCache)
|
16
|
+
@ds_cache = options.fetch(:ds_cache, ResoTransport::MetadataCache)
|
17
|
+
@connection = establish_connection
|
18
|
+
end
|
19
|
+
|
20
|
+
def establish_connection
|
21
|
+
Faraday.new(@endpoint, @faraday_options) do |faraday|
|
14
22
|
faraday.request :url_encoded
|
15
23
|
faraday.response :logger, @logger || ResoTransport.configuration.logger
|
16
|
-
#yield faraday if block_given?
|
17
24
|
faraday.use Authentication::Middleware, @authentication
|
18
|
-
faraday.adapter Faraday.default_adapter #unless faraday.builder.send(:adapter_set?)
|
25
|
+
faraday.adapter Faraday.default_adapter # unless faraday.builder.send(:adapter_set?)
|
19
26
|
end
|
20
27
|
end
|
21
28
|
|
22
29
|
def resources
|
23
|
-
@resources ||= metadata.entity_sets.map {|es| {es.name =>
|
30
|
+
@resources ||= metadata.entity_sets.map { |es| { es.name => resource_for(es) } }.reduce(:merge!)
|
31
|
+
end
|
32
|
+
|
33
|
+
def resource_for(entity_set)
|
34
|
+
localizations = {}
|
35
|
+
localizations = datasystem.localizations_for(entity_set.entity_type) if metadata.datasystem?
|
36
|
+
|
37
|
+
Resource.new(self, entity_set, localizations)
|
24
38
|
end
|
25
39
|
|
26
40
|
def metadata
|
27
41
|
@metadata ||= Metadata.new(self)
|
28
42
|
end
|
29
43
|
|
44
|
+
def datasystem
|
45
|
+
@datasystem ||= Datasystem.new(self)
|
46
|
+
end
|
47
|
+
|
30
48
|
def to_s
|
31
|
-
%(#<ResoTransport::Client endpoint="#{endpoint}", md_file="#{md_file}">)
|
49
|
+
%(#<ResoTransport::Client endpoint="#{endpoint}", md_file="#{md_file}", ds_file="#{ds_file}">)
|
32
50
|
end
|
33
51
|
|
34
52
|
def inspect
|
@@ -40,7 +58,7 @@ module ResoTransport
|
|
40
58
|
def ensure_valid_auth_strategy(options)
|
41
59
|
case options
|
42
60
|
when Hash
|
43
|
-
if options.
|
61
|
+
if options.key?(:endpoint)
|
44
62
|
Authentication::FetchTokenAuth.new(options)
|
45
63
|
else
|
46
64
|
Authentication::StaticTokenAuth.new(options)
|
@@ -49,6 +67,5 @@ module ResoTransport
|
|
49
67
|
raise ArgumentError, "#{options.inspect} invalid: cannot determine strategy"
|
50
68
|
end
|
51
69
|
end
|
52
|
-
|
53
70
|
end
|
54
71
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require_relative 'base_metadata'
|
2
|
+
|
3
|
+
module ResoTransport
|
4
|
+
class Datasystem < BaseMetadata
|
5
|
+
def initialize(client)
|
6
|
+
super client
|
7
|
+
@prefix = 'ds'
|
8
|
+
@classname = self.class.name
|
9
|
+
end
|
10
|
+
|
11
|
+
def localizations_for(resource_name)
|
12
|
+
localizations = parser.resources.dig(resource_name, 'Localizations') || []
|
13
|
+
localizations.map { |l| [l['Name'], l] }.to_h
|
14
|
+
end
|
15
|
+
|
16
|
+
def response
|
17
|
+
@response ||= client.connection.get('DataSystem') do |req|
|
18
|
+
req.headers['Accept'] = 'application/json'
|
19
|
+
@request = req
|
20
|
+
end
|
21
|
+
rescue Faraday::ConnectionFailed
|
22
|
+
raise NoResponse.new(request, nil, 'DataSystem')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ResoTransport
|
2
|
+
class DatasystemParser
|
3
|
+
def parse(doc)
|
4
|
+
begin
|
5
|
+
data = doc.is_a?(File) ? doc.read : doc
|
6
|
+
@json = JSON.parse data
|
7
|
+
rescue JSON::ParserError => e
|
8
|
+
@json = {}
|
9
|
+
puts e.message
|
10
|
+
end
|
11
|
+
self
|
12
|
+
end
|
13
|
+
|
14
|
+
# value ->
|
15
|
+
# Resources ->
|
16
|
+
# Name ->
|
17
|
+
# ResourcePath ->
|
18
|
+
# Localizations ->
|
19
|
+
# Name ->
|
20
|
+
# ResourcePath ->
|
21
|
+
|
22
|
+
def resources
|
23
|
+
@resources ||= @json['value'].map { |v| v['Resources'] }.flatten.compact.map { |r| [r['Name'], r] }.to_h
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -1,11 +1,9 @@
|
|
1
1
|
module ResoTransport
|
2
2
|
EntitySet = Struct.new(:name, :schema, :entity_type) do
|
3
|
-
|
4
3
|
def self.from_stream(args)
|
5
|
-
schema, entity_type = ResoTransport.split_schema_and_class_name(args[
|
4
|
+
schema, entity_type = ResoTransport.split_schema_and_class_name(args['EntityType'])
|
6
5
|
|
7
|
-
new(args[
|
6
|
+
new(args['Name'], schema, entity_type)
|
8
7
|
end
|
9
|
-
|
10
8
|
end
|
11
9
|
end
|
@@ -1,30 +1,29 @@
|
|
1
1
|
module ResoTransport
|
2
2
|
EntityType = Struct.new(:name, :base_type, :primary_key, :schema) do
|
3
|
-
|
4
3
|
def self.from_stream(args)
|
5
|
-
new(args[
|
4
|
+
new(args['Name'], args['BaseType'])
|
6
5
|
end
|
7
6
|
|
8
7
|
def parse(record)
|
9
|
-
record.each_pair do |k,v|
|
8
|
+
record.each_pair do |k, v|
|
10
9
|
next if v.nil?
|
11
|
-
|
12
|
-
|
13
|
-
|
10
|
+
|
11
|
+
property = property_map[k] || navigation_property_map[k]
|
12
|
+
record[k] = property.parse(v) if property
|
14
13
|
end
|
15
14
|
end
|
16
15
|
|
17
16
|
def parse_value(record)
|
18
|
-
record.each_pair do |k,v|
|
17
|
+
record.each_pair do |k, v|
|
19
18
|
next if v.nil?
|
20
|
-
|
21
|
-
|
22
|
-
|
19
|
+
|
20
|
+
property = property_map[k] || navigation_property_map[k]
|
21
|
+
record[k] = property.parse(v) if property
|
23
22
|
end
|
24
23
|
end
|
25
24
|
|
26
25
|
def property_map
|
27
|
-
@property_map ||= properties.
|
26
|
+
@property_map ||= properties.each_with_object({}) { |p, hsh| hsh[p.name] = p; }
|
28
27
|
end
|
29
28
|
|
30
29
|
def properties
|
@@ -32,7 +31,7 @@ module ResoTransport
|
|
32
31
|
end
|
33
32
|
|
34
33
|
def navigation_property_map
|
35
|
-
@navigation_property_map ||= navigation_properties.
|
34
|
+
@navigation_property_map ||= navigation_properties.each_with_object({}) { |p, hsh| hsh[p.name] = p; }
|
36
35
|
end
|
37
36
|
|
38
37
|
def navigation_properties
|
@@ -42,6 +41,5 @@ module ResoTransport
|
|
42
41
|
def enumerations
|
43
42
|
@enumerations ||= []
|
44
43
|
end
|
45
|
-
|
46
44
|
end
|
47
45
|
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module ResoTransport
|
2
|
+
class ResourceError < StandardError
|
3
|
+
attr_reader :resource
|
4
|
+
|
5
|
+
def initialize(resource)
|
6
|
+
@resource = resource
|
7
|
+
super message
|
8
|
+
end
|
9
|
+
|
10
|
+
def resource_name
|
11
|
+
return resource.name if resource.respond_to?(:name)
|
12
|
+
|
13
|
+
resource || 'unknown'
|
14
|
+
end
|
15
|
+
|
16
|
+
def message
|
17
|
+
"Request error for #{resource_name}"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class EncodeError < ResourceError
|
22
|
+
def initialize(resource, property)
|
23
|
+
@property = property
|
24
|
+
super resource
|
25
|
+
end
|
26
|
+
|
27
|
+
def message
|
28
|
+
"Property #{@property} not found for #{resource_name}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class LocalizationRequired < ResourceError
|
33
|
+
def message
|
34
|
+
"Localization required for #{resource_name}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class RequestError < ResourceError
|
39
|
+
attr_reader :request, :response
|
40
|
+
|
41
|
+
def initialize(request, response, resource = nil)
|
42
|
+
@response = response.respond_to?(:to_hash) ? response.to_hash : response
|
43
|
+
@request = request
|
44
|
+
super resource
|
45
|
+
end
|
46
|
+
|
47
|
+
def message
|
48
|
+
"Received #{response[:status]} for #{resource_name}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class NoResponse < RequestError
|
53
|
+
def message
|
54
|
+
"No response for #{resource_name}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class ResponseError < RequestError
|
59
|
+
def message
|
60
|
+
"Request succeeded for #{resource_name} with response errors"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class AccessDenied < RequestError
|
65
|
+
def message
|
66
|
+
"Access denied: #{response.reason_phrase}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -1,10 +1,12 @@
|
|
1
|
-
|
2
|
-
Metadata = Struct.new(:client) do
|
1
|
+
require_relative 'base_metadata'
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
module ResoTransport
|
4
|
+
class Metadata < BaseMetadata
|
5
|
+
def initialize(client)
|
6
|
+
super client
|
7
|
+
@prefix = 'md'
|
8
|
+
@classname = self.class.name
|
9
|
+
end
|
8
10
|
|
9
11
|
def entity_sets
|
10
12
|
parser.entity_sets
|
@@ -14,36 +16,17 @@ module ResoTransport
|
|
14
16
|
parser.schemas
|
15
17
|
end
|
16
18
|
|
17
|
-
def
|
18
|
-
|
19
|
+
def datasystem?
|
20
|
+
parser.datasystem?
|
19
21
|
end
|
20
22
|
|
21
|
-
def
|
22
|
-
|
23
|
-
if File.exist?(client.md_file) && File.size(client.md_file) > 0
|
24
|
-
File.new(client.md_file)
|
25
|
-
else
|
26
|
-
raw_md = raw
|
27
|
-
File.open(client.md_file, "w") {|f| f.write(raw.force_encoding("UTF-8")) } unless raw_md.length == 0
|
28
|
-
File.new(client.md_file)
|
29
|
-
end
|
30
|
-
else
|
31
|
-
raw
|
32
|
-
end
|
33
|
-
end
|
34
|
-
|
35
|
-
def raw
|
36
|
-
resp = client.connection.get("$metadata") do |req|
|
23
|
+
def response
|
24
|
+
@response ||= client.connection.get('$metadata') do |req|
|
37
25
|
req.headers['Accept'] = MIME_TYPES[client.vendor.fetch(:metadata_format, :xml).to_sym]
|
26
|
+
@request = req
|
38
27
|
end
|
39
|
-
|
40
|
-
|
41
|
-
resp.body
|
42
|
-
else
|
43
|
-
puts resp.body
|
44
|
-
raise "Error getting metadata!"
|
45
|
-
end
|
28
|
+
rescue Faraday::ConnectionFailed
|
29
|
+
raise NoResponse.new(request, nil, '$metadata')
|
46
30
|
end
|
47
|
-
|
48
31
|
end
|
49
32
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ResoTransport
|
2
|
+
class MetadataCache
|
3
|
+
attr_reader :name
|
4
|
+
|
5
|
+
def initialize(name)
|
6
|
+
@name = name
|
7
|
+
end
|
8
|
+
|
9
|
+
def read
|
10
|
+
return nil if !File.exist?(name) || File.size(name).zero?
|
11
|
+
|
12
|
+
File.new(name)
|
13
|
+
end
|
14
|
+
|
15
|
+
def write(raw)
|
16
|
+
File.open(name, 'w') { |f| f.write(raw.force_encoding('UTF-8')) } if raw.length.positive?
|
17
|
+
File.new(name)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|