talis 0.12.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/.gitignore +12 -0
- data/.rspec +2 -0
- data/.rubocop.yml +2 -0
- data/.travis.yml +24 -0
- data/.yardopts +1 -0
- data/CONTRIBUTING.md +28 -0
- data/Gemfile +4 -0
- data/Guardfile +5 -0
- data/README.md +76 -0
- data/Rakefile +8 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/talis.rb +25 -0
- data/lib/talis/analytics.rb +31 -0
- data/lib/talis/analytics/event.rb +67 -0
- data/lib/talis/authentication.rb +14 -0
- data/lib/talis/authentication/client.rb +82 -0
- data/lib/talis/authentication/login.rb +169 -0
- data/lib/talis/authentication/public_key.rb +53 -0
- data/lib/talis/authentication/token.rb +172 -0
- data/lib/talis/bibliography.rb +52 -0
- data/lib/talis/bibliography/ebook.rb +50 -0
- data/lib/talis/bibliography/manifestation.rb +141 -0
- data/lib/talis/bibliography/result_set.rb +34 -0
- data/lib/talis/bibliography/work.rb +164 -0
- data/lib/talis/constants.rb +9 -0
- data/lib/talis/errors.rb +10 -0
- data/lib/talis/errors/authentication_failed_error.rb +4 -0
- data/lib/talis/errors/client_errors.rb +19 -0
- data/lib/talis/errors/server_communication_error.rb +4 -0
- data/lib/talis/errors/server_error.rb +4 -0
- data/lib/talis/extensions/object.rb +11 -0
- data/lib/talis/feeds.rb +8 -0
- data/lib/talis/feeds/annotation.rb +129 -0
- data/lib/talis/feeds/feed.rb +58 -0
- data/lib/talis/hierarchy.rb +9 -0
- data/lib/talis/hierarchy/asset.rb +265 -0
- data/lib/talis/hierarchy/node.rb +200 -0
- data/lib/talis/hierarchy/resource.rb +159 -0
- data/lib/talis/oauth_service.rb +18 -0
- data/lib/talis/resource.rb +68 -0
- data/lib/talis/user.rb +112 -0
- data/lib/talis/version.rb +3 -0
- data/talis.gemspec +39 -0
- metadata +327 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
module Talis
|
3
|
+
module Bibliography
|
4
|
+
# Provides convenience methods for working with MetatronClient::*ResultSets
|
5
|
+
# TODO: eventually this will be replaced with PaginatedJsonApiArray from
|
6
|
+
# Atlas
|
7
|
+
module ResultSet
|
8
|
+
include Enumerable
|
9
|
+
extend Forwardable
|
10
|
+
def_delegators :@data, :[], :each, :first, :last, :find, :find_all
|
11
|
+
|
12
|
+
# Replaces the MetatronClient objects with more intuitive DSL objects
|
13
|
+
# and hydrates the relationships from the included array, if available
|
14
|
+
def hydrate
|
15
|
+
model_data
|
16
|
+
self
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def model_data
|
22
|
+
data.each_with_index do |data, i|
|
23
|
+
if is_a? MetatronClient::WorkResultSet
|
24
|
+
resource = Work.new data
|
25
|
+
elsif is_a? MetatronClient::ManifestationResultSet
|
26
|
+
resource = Manifestation.new data
|
27
|
+
end
|
28
|
+
resource.hydrate_relationships(included) if included
|
29
|
+
self.data[i] = resource
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
require 'metatron_ruby_client'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
module Talis
|
5
|
+
module Bibliography
|
6
|
+
# Represents bibliographic works API operations provided by the Metatron gem
|
7
|
+
# {https://github.com/talis/metatron_rb}
|
8
|
+
#
|
9
|
+
# In order to perform remote operations, the client must be configured
|
10
|
+
# with a valid OAuth client that is allowed to query nodes:
|
11
|
+
#
|
12
|
+
# Talis::Authentication.client_id = 'client_id'
|
13
|
+
# Talis::Authentication.client_secret = 'client_secret'
|
14
|
+
#
|
15
|
+
class Work < Talis::Resource
|
16
|
+
extend Forwardable, Talis::OAuthService, Talis::Bibliography
|
17
|
+
base_uri Talis::METATRON_HOST
|
18
|
+
attr_reader :manifestations, :assets, :work_data
|
19
|
+
attr_accessor :id, :type, :title
|
20
|
+
def_delegators :@work_data, :id, :type
|
21
|
+
|
22
|
+
# rubocop:disable Metrics/LineLength
|
23
|
+
class << self
|
24
|
+
# Search for bibliographic works
|
25
|
+
# @param request_id [String] ('uuid') unique ID for the remote request.
|
26
|
+
# @param query [String] the query to filter works on
|
27
|
+
# @param include [Array] the related resources to associate with each work
|
28
|
+
# see {https://github.com/talis/metatron_rb/blob/master/docs/DefaultApi.md#2_works_get}
|
29
|
+
# @param opts [Hash] Can include key/value pairs for offset (default: 0),
|
30
|
+
# limit (default 20), and escape_query (boolean), which will escape any
|
31
|
+
# reserved characters from the query parameter
|
32
|
+
# @return [MetatronClient::WorkResultSet] containing data and meta attributes.
|
33
|
+
# The structure is as follows:
|
34
|
+
# {
|
35
|
+
# data: [work1, work2, work3, work4, work5],
|
36
|
+
# meta: { offset: 0, count: 20, limit: 5 }
|
37
|
+
# included: [manifestation|assset1, manifestation|assset2]
|
38
|
+
# }
|
39
|
+
# where works are of type Talis::Bibliography::Work, which are also available
|
40
|
+
# directly via the Enumerable methods: each, find, find_all, first, last
|
41
|
+
# @raise [Talis::ClientError] if the request was invalid.
|
42
|
+
# @raise [Talis::ServerError] if the search failed on the
|
43
|
+
# server.
|
44
|
+
# @raise [Talis::ServerCommunicationError] for network issues.
|
45
|
+
def find(request_id: new_req_id, query:, include: [], opts: {})
|
46
|
+
query = escape_query(query) if opts[:escape_query]
|
47
|
+
offset = opts[:offset] || 0
|
48
|
+
limit = opts[:limit] || 20
|
49
|
+
search_works(request_id, query, offset, limit, include)
|
50
|
+
rescue MetatronClient::ApiError => error
|
51
|
+
begin
|
52
|
+
handle_response(error)
|
53
|
+
rescue Talis::NotFoundError
|
54
|
+
empty_result(offset, limit)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Fetch a single work by id
|
59
|
+
# @param request_id [String] ('uuid') unique ID for the remote request.
|
60
|
+
# @param id [String] the ID of the work to fetch.
|
61
|
+
# @return Talis::Bibliography::Work or nil if the work cannot be found.
|
62
|
+
# @raise [Talis::ClientError] if the request was invalid.
|
63
|
+
# @raise [Talis::ServerError] if the fetch failed on the
|
64
|
+
# server.
|
65
|
+
# @raise [Talis::ServerCommunicationError] for network issues.
|
66
|
+
def get(request_id: new_req_id, id:)
|
67
|
+
new api_client(request_id).works_work_id_assets_get(id, token).data
|
68
|
+
rescue MetatronClient::ApiError => error
|
69
|
+
begin
|
70
|
+
handle_response(error)
|
71
|
+
rescue Talis::NotFoundError
|
72
|
+
nil
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def search_works(request_id, query, offset, limit, include)
|
79
|
+
api_client(request_id).work(token, query, limit,
|
80
|
+
offset, include: include)
|
81
|
+
.extend(ResultSet).hydrate
|
82
|
+
end
|
83
|
+
|
84
|
+
def empty_result(offset, limit)
|
85
|
+
empty_result_set(MetatronClient::WorkResultSet,
|
86
|
+
offset: offset, limit: limit, count: 0)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def initialize(work_data = nil)
|
91
|
+
if work_data.is_a? MetatronClient::WorkData
|
92
|
+
parse_work_data work_data
|
93
|
+
else
|
94
|
+
@work_data = MetatronClient::WorkData.new
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# TODO: call manifestation route if not set
|
99
|
+
def manifestations
|
100
|
+
@manifestations ||= []
|
101
|
+
end
|
102
|
+
|
103
|
+
# TODO: call assets route if not set
|
104
|
+
def assets
|
105
|
+
@assets ||= []
|
106
|
+
end
|
107
|
+
|
108
|
+
# By default, the metatron client returns generic ResourceLink objects
|
109
|
+
# as the related resources. When passed an array of Metatron::ResourceData
|
110
|
+
# objects, it will replace the ResourceLink objects with more appropriately
|
111
|
+
# typed objects
|
112
|
+
# @param resources [Array] an array of Metatron::ResourceData objects
|
113
|
+
def hydrate_relationships(resources)
|
114
|
+
manifestations.map! do |m|
|
115
|
+
resource = find_relationship_in_included m.manifestation_data.to_hash,
|
116
|
+
resources
|
117
|
+
return m unless resource
|
118
|
+
hydrate_manifestation_assets resource, resources
|
119
|
+
Manifestation.new(MetatronClient::ManifestationData.new(
|
120
|
+
resource.to_hash
|
121
|
+
))
|
122
|
+
end
|
123
|
+
nil
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
def hydrate_manifestation_assets(manifestation, resources)
|
129
|
+
if manifestation_has_assets? manifestation
|
130
|
+
manifestation.relationships[:assets][:data].each do |asset_data|
|
131
|
+
asset = find_relationship_in_included asset_data.to_hash, resources
|
132
|
+
assets << MetatronClient::AssetData.new(asset.to_hash) if asset
|
133
|
+
end
|
134
|
+
end
|
135
|
+
nil
|
136
|
+
end
|
137
|
+
|
138
|
+
def manifestation_has_assets?(manifestation)
|
139
|
+
manifestation.try(:relationships) &&
|
140
|
+
manifestation.relationships[:assets] &&
|
141
|
+
manifestation.relationships[:assets][:data]
|
142
|
+
end
|
143
|
+
|
144
|
+
def find_relationship_in_included(resource_data, included)
|
145
|
+
included.find do |resource|
|
146
|
+
resource.id == resource_data[:id] && resource.type == resource_data[:type]
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def parse_work_data(work_data)
|
151
|
+
@work_data = work_data
|
152
|
+
@title = work_data.try(:attributes).try(:titles)
|
153
|
+
|
154
|
+
if work_data.try(:relationships).try(:manifestations).try(:data)
|
155
|
+
work_data.relationships.manifestations.data.each do |manifestation|
|
156
|
+
manifestations << Manifestation.new(
|
157
|
+
MetatronClient::ManifestationData.new(manifestation.to_hash)
|
158
|
+
)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
module Talis
|
2
|
+
BABEL_HOST = 'https://feeds.talis.com'.freeze
|
3
|
+
BLUEPRINT_HOST = 'https://hierarchy.talis.com'.freeze
|
4
|
+
ECHO_HOST = 'https://analytics.talis.com'.freeze
|
5
|
+
METATRON_HOST = 'https://bibliography.talis.com'.freeze
|
6
|
+
PERSONA_HOST = 'https://users.talis.com'.freeze
|
7
|
+
|
8
|
+
EBOOK_TYPE = 'ebook_inventory'.freeze
|
9
|
+
end
|
data/lib/talis/errors.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
module Talis
|
2
|
+
# All errors will extend from this base class.
|
3
|
+
class Error < StandardError
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
require_relative 'errors/authentication_failed_error'
|
8
|
+
require_relative 'errors/client_errors'
|
9
|
+
require_relative 'errors/server_error'
|
10
|
+
require_relative 'errors/server_communication_error'
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Talis
|
2
|
+
class ClientError < Talis::Error
|
3
|
+
end
|
4
|
+
|
5
|
+
class BadRequestError < ClientError
|
6
|
+
end
|
7
|
+
|
8
|
+
class UnauthorizedError < ClientError
|
9
|
+
end
|
10
|
+
|
11
|
+
class ForbiddenError < ClientError
|
12
|
+
end
|
13
|
+
|
14
|
+
class NotFoundError < ClientError
|
15
|
+
end
|
16
|
+
|
17
|
+
class ConflictError < ClientError
|
18
|
+
end
|
19
|
+
end
|
data/lib/talis/feeds.rb
ADDED
@@ -0,0 +1,129 @@
|
|
1
|
+
module Talis
|
2
|
+
module Feeds
|
3
|
+
# Represents an annotation, for example, a video resource comment.
|
4
|
+
class Annotation < Talis::Resource
|
5
|
+
base_uri Talis::BABEL_HOST
|
6
|
+
attr_reader :id, :body, :target, :annotated_by, :motivated_by,
|
7
|
+
:expires_at, :user
|
8
|
+
|
9
|
+
# Creates a new annotation object. For internal use only,
|
10
|
+
# use {Annotation.create}.
|
11
|
+
# @param data [Hash] The incoming annotation data to construct
|
12
|
+
# a new annotation object with.
|
13
|
+
# @param user [Talis::User](nil) The user that created the annotation.
|
14
|
+
# This will be nil for an annotation that has not been hydrated.
|
15
|
+
# Hydrated data is provided by feeds.
|
16
|
+
def initialize(data, user: nil)
|
17
|
+
# Symbolise all keys for consistency between what was provided
|
18
|
+
# and what is returned.
|
19
|
+
data = JSON.parse(JSON[data], symbolize_names: true)
|
20
|
+
@id = data[:_id]
|
21
|
+
@body = data[:hasBody]
|
22
|
+
@target = data[:hasTarget].map { |target| underscore(target) }
|
23
|
+
@annotated_by = data[:annotatedBy]
|
24
|
+
@motivated_by = data[:motivatedBy]
|
25
|
+
@expires_at = data[:expiresAt]
|
26
|
+
@user = user
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def underscore(payload)
|
32
|
+
payload.map { |key, value| [key.to_s.underscore.to_sym, value] }.to_h
|
33
|
+
end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
# Create a new annotation that is persisted.
|
37
|
+
# @param opts [Hash] The annotation data used to create the annotation:
|
38
|
+
# {
|
39
|
+
# request_id: 'optional unique ID for the remote request'
|
40
|
+
# body: {
|
41
|
+
# format: 'e.g: text/plain',
|
42
|
+
# type: 'e.g: Text',
|
43
|
+
# chars: 'annotation content',
|
44
|
+
# details: {} # optional hash to provide additional details.
|
45
|
+
# },
|
46
|
+
# target: [
|
47
|
+
# {
|
48
|
+
# uri: 'location of target being annotated',
|
49
|
+
# as_referenced_by: 'optional reference location',
|
50
|
+
# fragment: 'optional fragment within location'
|
51
|
+
# }
|
52
|
+
# ]
|
53
|
+
# annotated_by: 'Talis user GUID performing the annotation',
|
54
|
+
# motivated_by: 'optional motivation string',
|
55
|
+
# expires_at: 'optional ISO 8601 date time string'
|
56
|
+
# }
|
57
|
+
# @return [Talis::Feeds::Annotation] The persisted annotation.
|
58
|
+
# @raise [Talis::ClientError] if the request was invalid.
|
59
|
+
# @raise [Talis::ServerError] if there was a problem with the request.
|
60
|
+
# @raise [Talis::ServerCommunicationError] for network issues.
|
61
|
+
# @raise [ArgumentError] for validation errors against opts.
|
62
|
+
def create(opts)
|
63
|
+
validate_annotation opts
|
64
|
+
new handle_response(post_annotation(opts))
|
65
|
+
rescue SocketError
|
66
|
+
raise Talis::ServerCommunicationError
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def validate_annotation(annotation)
|
72
|
+
min_required = 'annotation must contain body, target and annotated_by'
|
73
|
+
required = [:body, :target, :annotated_by]
|
74
|
+
provided = annotation.select { |attr| required.include? attr }.keys
|
75
|
+
raise ArgumentError, min_required unless required == provided
|
76
|
+
validate_targets(annotation)
|
77
|
+
validate_expiry(annotation)
|
78
|
+
end
|
79
|
+
|
80
|
+
def validate_targets(annotation)
|
81
|
+
targets = annotation[:target]
|
82
|
+
raise ArgumentError,
|
83
|
+
'annotation target must be an array' unless targets.is_a? Array
|
84
|
+
targets.each do |target|
|
85
|
+
raise ArgumentError,
|
86
|
+
'annotation targets must contain uri' unless target[:uri]
|
87
|
+
raise ArgumentError,
|
88
|
+
'target uri must be a string' unless target[:uri].is_a? String
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def validate_expiry(annotation)
|
93
|
+
expiry = annotation[:expires_at]
|
94
|
+
Time.iso8601(expiry) if expiry
|
95
|
+
rescue StandardError
|
96
|
+
raise ArgumentError, 'expired_at must be a valid ISO 8601 date'
|
97
|
+
end
|
98
|
+
|
99
|
+
def post_annotation(opts)
|
100
|
+
request_id = opts[:request_id] || new_req_id
|
101
|
+
post('/annotations',
|
102
|
+
headers: {
|
103
|
+
'Content-Type' => 'application/json',
|
104
|
+
'X-Request-Id' => request_id,
|
105
|
+
'Authorization' => "Bearer #{token}"
|
106
|
+
},
|
107
|
+
body: body(opts).to_json)
|
108
|
+
end
|
109
|
+
|
110
|
+
def body(opts)
|
111
|
+
min_body = {
|
112
|
+
hasBody: opts[:body],
|
113
|
+
hasTarget: opts[:target].map { |target| camelize(target) },
|
114
|
+
annotatedBy: opts[:annotated_by]
|
115
|
+
}
|
116
|
+
min_body[:expiresAt] = opts[:expires_at] if opts[:expires_at]
|
117
|
+
min_body[:motivatedBy] = opts[:motivated_by] if opts[:motivated_by]
|
118
|
+
min_body
|
119
|
+
end
|
120
|
+
|
121
|
+
def camelize(payload)
|
122
|
+
payload.map do |key, value|
|
123
|
+
[key.to_s.camelize(:lower).to_sym, value]
|
124
|
+
end.to_h
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Talis
|
2
|
+
module Feeds
|
3
|
+
# Represents a feed, as a way of bringing back a collection of annotations.
|
4
|
+
class Feed < Talis::Resource
|
5
|
+
base_uri Talis::BABEL_HOST
|
6
|
+
|
7
|
+
class << self
|
8
|
+
# Returns a collection of annotations matching the provided target.
|
9
|
+
# @param request_id [String] ('uuid') unique ID for the remote request.
|
10
|
+
# @param target_uri [String] The URI uniquely identifying the target.
|
11
|
+
# @return [Array<Talis::Feeds::Annotation>] An array of annotations
|
12
|
+
# or an empty array if the feed was not found.
|
13
|
+
# @raise [Talis::ClientError] if the request was invalid.
|
14
|
+
# @raise [Talis::ServerError] if there was a problem with the request.
|
15
|
+
# @raise [Talis::ServerCommunicationError] for network issues.
|
16
|
+
def find(request_id: new_req_id, target_uri:)
|
17
|
+
md5_target_uri = Digest::MD5.hexdigest(target_uri)
|
18
|
+
response = fetch_feed(request_id, md5_target_uri)
|
19
|
+
begin
|
20
|
+
build handle_response(response)
|
21
|
+
rescue Talis::NotFoundError
|
22
|
+
[]
|
23
|
+
end
|
24
|
+
rescue SocketError
|
25
|
+
raise Talis::ServerCommunicationError
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def fetch_feed(request_id, md5_target_uri)
|
31
|
+
get("/feeds/targets/#{md5_target_uri}/activity/annotations/hydrate",
|
32
|
+
headers: {
|
33
|
+
'Content-Type' => 'application/json',
|
34
|
+
'X-Request-Id' => request_id,
|
35
|
+
'Authorization' => "Bearer #{token}"
|
36
|
+
})
|
37
|
+
end
|
38
|
+
|
39
|
+
def build(response)
|
40
|
+
annotations = response['annotations']
|
41
|
+
annotations.map do |annotation|
|
42
|
+
Annotation.new(annotation, user: hydrate_user(annotation, response))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def hydrate_user(annotation_data, feed_data)
|
47
|
+
profiles = feed_data['userProfiles']
|
48
|
+
return unless profiles
|
49
|
+
guid = annotation_data['annotatedBy']
|
50
|
+
user = profiles[guid]
|
51
|
+
return unless user
|
52
|
+
Talis::User.build(guid: guid, first_name: user['first_name'],
|
53
|
+
surname: user['surname'], email: user['email'])
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|