cogitate 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +69 -15
- data/lib/cogitate.rb +18 -2
- data/lib/cogitate/README.md +8 -0
- data/lib/cogitate/client.rb +97 -0
- data/lib/cogitate/client/agent_builder.rb +86 -0
- data/lib/cogitate/client/data_to_object_coercer.rb +38 -0
- data/lib/cogitate/client/exceptions.rb +10 -0
- data/lib/cogitate/client/identifier_builder.rb +26 -0
- data/lib/cogitate/client/request.rb +56 -0
- data/lib/cogitate/client/response_parsers.rb +19 -0
- data/lib/cogitate/client/response_parsers/agents_with_detailed_identifiers_extractor.rb +15 -0
- data/lib/cogitate/client/response_parsers/agents_without_group_membership_extractor.rb +23 -0
- data/lib/cogitate/client/response_parsers/basic_extractor.rb +14 -0
- data/lib/cogitate/client/response_parsers/email_extractor.rb +17 -0
- data/lib/cogitate/client/retrieve_agent_from_ticket.rb +43 -0
- data/lib/cogitate/client/ticket_to_token_coercer.rb +35 -0
- data/lib/cogitate/client/token_to_object_coercer.rb +38 -0
- data/lib/cogitate/configuration.rb +99 -0
- data/lib/cogitate/exceptions.rb +25 -0
- data/lib/cogitate/generators/install_generator.rb +10 -0
- data/lib/cogitate/generators/templates/cogitate_initializer.rb.erb +17 -0
- data/lib/cogitate/interfaces.rb +40 -0
- data/lib/cogitate/models.rb +6 -0
- data/lib/cogitate/models/agent.rb +143 -0
- data/lib/cogitate/models/agent/serializer.rb +75 -0
- data/lib/cogitate/models/agent/with_token.rb +29 -0
- data/lib/cogitate/models/identifier.rb +114 -0
- data/lib/cogitate/models/identifiers.rb +7 -0
- data/lib/cogitate/models/identifiers/with_attribute_hash.rb +44 -0
- data/lib/cogitate/railtie.rb +8 -0
- data/lib/cogitate/services/identifiers_decoder.rb +61 -0
- data/lib/cogitate/services/tokenizer.rb +80 -0
- metadata +105 -5
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'cogitate/interfaces'
|
2
|
+
require 'cogitate/client'
|
3
|
+
require 'cogitate/models/identifier'
|
4
|
+
require 'base64'
|
5
|
+
|
6
|
+
module Cogitate
|
7
|
+
module Models
|
8
|
+
# @api public
|
9
|
+
#
|
10
|
+
# An Agent is a "bucket" of attributes. It represents a single acting entity:
|
11
|
+
#
|
12
|
+
# * a Person - the human clacking away at the keyboard requesting things
|
13
|
+
# * a Service - an application that "does stuff" to data
|
14
|
+
#
|
15
|
+
# @todo Consider adding #to_token so that an Agent can fulfill the AgentWithToken interface
|
16
|
+
class Agent
|
17
|
+
# @api public
|
18
|
+
def self.build_with_identifying_information(strategy:, identifying_value:, **keywords, &block)
|
19
|
+
identifier = Cogitate::Models::Identifier.new(strategy: strategy, identifying_value: identifying_value)
|
20
|
+
new(identifier: identifier, **keywords, &block)
|
21
|
+
end
|
22
|
+
|
23
|
+
# @api public
|
24
|
+
def self.build_with_encoded_id(encoded_identifier:)
|
25
|
+
strategy, identifying_value = Cogitate::Client.extract_strategy_and_identifying_value(encoded_identifier)
|
26
|
+
build_with_identifying_information(strategy: strategy, identifying_value: identifying_value)
|
27
|
+
end
|
28
|
+
|
29
|
+
include Contracts
|
30
|
+
Contract(
|
31
|
+
Contracts::KeywordArgs[identifier: Cogitate::Interfaces::IdentifierInterface], Contracts::Any =>
|
32
|
+
Cogitate::Interfaces::AgentInterface
|
33
|
+
)
|
34
|
+
# @note I'm choosing the :identifier as the input and :primary_identifier as the attribute. I believe it is possible that during the
|
35
|
+
# process that builds the Agent, I may encounter a more applicable primary_identifier.
|
36
|
+
def initialize(identifier:, container: default_container, serializer_builder: default_serializer_builder)
|
37
|
+
self.identifiers = container.new
|
38
|
+
self.verified_identifiers = container.new
|
39
|
+
self.primary_identifier = identifier
|
40
|
+
self.serializer = serializer_builder.call(agent: self)
|
41
|
+
self.emails = container.new
|
42
|
+
yield(self) if block_given?
|
43
|
+
self
|
44
|
+
end
|
45
|
+
|
46
|
+
Contract(Cogitate::Interfaces::IdentifierInterface => Cogitate::Interfaces::IdentifierInterface)
|
47
|
+
# @api public
|
48
|
+
#
|
49
|
+
# Add an identifier to the agent.
|
50
|
+
#
|
51
|
+
# @param identifier [Cogitate::Interfaces::IdentifierInterface]
|
52
|
+
# @return [Cogitate::Interfaces::IdentifierInterface]
|
53
|
+
def add_identifier(identifier)
|
54
|
+
identifiers << identifier
|
55
|
+
identifier
|
56
|
+
end
|
57
|
+
|
58
|
+
# @api public
|
59
|
+
# @return [Enumerator, nil] nil if a block is given else an Enumerator
|
60
|
+
def with_identifiers
|
61
|
+
return enum_for(:with_identifiers) unless block_given?
|
62
|
+
identifiers.each { |identifier| yield(identifier) }
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
|
66
|
+
Contract(Cogitate::Interfaces::IdentifierInterface => Cogitate::Interfaces::IdentifierInterface)
|
67
|
+
# @api public
|
68
|
+
#
|
69
|
+
# Add a verified identifier to the agent.
|
70
|
+
#
|
71
|
+
# @param identifier [Cogitate::Interfaces::IdentifierInterface]
|
72
|
+
# @return [Cogitate::Interfaces::IdentifierInterface]
|
73
|
+
def add_verified_identifier(identifier)
|
74
|
+
verified_identifiers << identifier
|
75
|
+
identifier
|
76
|
+
end
|
77
|
+
|
78
|
+
# @api public
|
79
|
+
# @return [Enumerator, nil] nil if a block is given else an Enumerator
|
80
|
+
def with_verified_identifiers
|
81
|
+
return enum_for(:with_verified_identifiers) unless block_given?
|
82
|
+
verified_identifiers.each { |identifier| yield(identifier) }
|
83
|
+
nil
|
84
|
+
end
|
85
|
+
|
86
|
+
# @api private
|
87
|
+
# I am not yet set on this method being part of the public API.
|
88
|
+
def add_email(input)
|
89
|
+
emails << input
|
90
|
+
end
|
91
|
+
|
92
|
+
# @api public
|
93
|
+
# @return [Enumerator, nil] nil if a block is given else an Enumerator
|
94
|
+
def with_emails
|
95
|
+
return enum_for(:with_emails) unless block_given?
|
96
|
+
emails.each { |email| yield(email) }
|
97
|
+
nil
|
98
|
+
end
|
99
|
+
|
100
|
+
# @return [Cogitate::Interfaces::IdentifierInterface] What has been assigned as the primary identifier of this agent.
|
101
|
+
attr_reader :primary_identifier
|
102
|
+
|
103
|
+
extend Forwardable
|
104
|
+
def_delegators :primary_identifier, *Cogitate::Models::Identifier.interface_method_names
|
105
|
+
def_delegators :serializer, :as_json
|
106
|
+
|
107
|
+
# Consider the scenario in which a student assigns a faculty member (via an email address) as a collaborating
|
108
|
+
# researcher. That faculty member should have permission to collaborate on the student's work. The permissions
|
109
|
+
# are stored as the Faculty's email. Then the Faculty authenticates with a NetID (a canonical identifier for the
|
110
|
+
# institution that stood up this Cogitate instance) and associates the NetID with their email address.
|
111
|
+
#
|
112
|
+
# At that point we want to allow objects associated with both the NetID and connected email to be available to
|
113
|
+
# the faculty.
|
114
|
+
#
|
115
|
+
# @api public
|
116
|
+
# @return [Array] of ids for each of the verified identifiers
|
117
|
+
def ids
|
118
|
+
[id] + with_verified_identifiers.map(&:id)
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
attr_accessor :identifiers
|
124
|
+
attr_accessor :verified_identifiers
|
125
|
+
attr_accessor :emails
|
126
|
+
|
127
|
+
Contract(Cogitate::Interfaces::IdentifierInterface => Contracts::Any)
|
128
|
+
attr_writer :primary_identifier
|
129
|
+
|
130
|
+
def default_container
|
131
|
+
require 'set' unless defined?(Set)
|
132
|
+
Set
|
133
|
+
end
|
134
|
+
|
135
|
+
attr_accessor :serializer
|
136
|
+
# @todo This is something that needs greater consideration. Is it applicable for both client and server?
|
137
|
+
def default_serializer_builder
|
138
|
+
require 'cogitate/models/agent/serializer' unless defined?(Cogitate::Models::Agent::Serializer)
|
139
|
+
Cogitate::Models::Agent::Serializer.method(:new)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'figaro'
|
2
|
+
require 'set'
|
3
|
+
require 'cogitate/models/agent'
|
4
|
+
|
5
|
+
module Cogitate
|
6
|
+
module Models
|
7
|
+
class Agent
|
8
|
+
# Responsible for the serialization of an Agent
|
9
|
+
class Serializer
|
10
|
+
# @api public
|
11
|
+
def initialize(agent:)
|
12
|
+
self.agent = agent
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
attr_accessor :agent
|
18
|
+
|
19
|
+
public
|
20
|
+
|
21
|
+
def as_json(*)
|
22
|
+
prepare_relationships_and_inclusions!
|
23
|
+
{
|
24
|
+
'type' => JSON_API_TYPE, 'id' => agent.encoded_id,
|
25
|
+
'links' => { 'self' => "#{url_for_identifier(agent.encoded_id)}" },
|
26
|
+
'attributes' => { 'strategy' => agent.strategy, 'identifying_value' => agent.identifying_value, 'emails' => emails_as_json },
|
27
|
+
'relationships' => { 'identifiers' => identities_as_json, 'verified_identifiers' => verified_identities_as_json },
|
28
|
+
'included' => included_objects_as_json
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
JSON_API_TYPE = 'agents'.freeze
|
35
|
+
def type
|
36
|
+
JSON_API_TYPE
|
37
|
+
end
|
38
|
+
|
39
|
+
def emails_as_json
|
40
|
+
agent.with_emails.each_with_object([]) do |email, mem|
|
41
|
+
mem << email
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# @note This method is rather complicated but it reduces the number of times we iterate
|
46
|
+
# through each of the Enumerators.
|
47
|
+
def prepare_relationships_and_inclusions!
|
48
|
+
included_objects_as_json = Set.new
|
49
|
+
identities_as_json = Set.new
|
50
|
+
verified_identities_as_json = Set.new
|
51
|
+
|
52
|
+
agent.with_identifiers do |identifier|
|
53
|
+
identities_as_json << { 'type' => 'identifiers', 'id' => identifier.encoded_id }
|
54
|
+
included_objects_as_json << { 'type' => 'identifiers', 'id' => identifier.encoded_id, 'attributes' => identifier.as_json }
|
55
|
+
end
|
56
|
+
|
57
|
+
agent.with_verified_identifiers do |identifier|
|
58
|
+
verified_identities_as_json << { 'type' => 'identifiers', 'id' => identifier.encoded_id }
|
59
|
+
included_objects_as_json << { 'type' => 'identifiers', 'id' => identifier.encoded_id, 'attributes' => identifier.as_json }
|
60
|
+
end
|
61
|
+
|
62
|
+
@included_objects_as_json = included_objects_as_json.to_a
|
63
|
+
@identities_as_json = identities_as_json.to_a
|
64
|
+
@verified_identities_as_json = verified_identities_as_json.to_a
|
65
|
+
end
|
66
|
+
|
67
|
+
attr_reader :included_objects_as_json, :identities_as_json, :verified_identities_as_json
|
68
|
+
|
69
|
+
def url_for_identifier(encoded_identifier)
|
70
|
+
"#{Figaro.env.protocol}://#{Figaro.env.domain_name}/api/agents/#{encoded_identifier}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'cogitate/interfaces'
|
2
|
+
|
3
|
+
module Cogitate
|
4
|
+
module Models
|
5
|
+
class Agent
|
6
|
+
# A parameter style class that can be sent to the client of Cogitate.
|
7
|
+
#
|
8
|
+
# The token, if decoded and parsed will be the given agent. Likewise the agent, if encoded would be the given token.
|
9
|
+
class WithToken < SimpleDelegator
|
10
|
+
include Contracts
|
11
|
+
Contract(KeywordArgs[agent: Cogitate::Interfaces::AgentInterface, token: String] => Any)
|
12
|
+
def initialize(agent:, token:)
|
13
|
+
self.token = token
|
14
|
+
self.agent = agent
|
15
|
+
super(agent)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
attr_accessor :agent, :token
|
21
|
+
|
22
|
+
public
|
23
|
+
|
24
|
+
alias_method :to_token, :token
|
25
|
+
public :to_token
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'contracts'
|
2
|
+
require 'cogitate/interfaces'
|
3
|
+
require 'base64'
|
4
|
+
|
5
|
+
module Cogitate
|
6
|
+
module Models
|
7
|
+
# @api public
|
8
|
+
#
|
9
|
+
# A parameter object that defines how we go from decoding identifiers to extracting an identity.
|
10
|
+
class Identifier
|
11
|
+
INTERFACE_METHOD_NAMES = [:identifying_value, :<=>, :encoded_id, :id, :strategy, :name].freeze
|
12
|
+
def self.interface_method_names
|
13
|
+
INTERFACE_METHOD_NAMES
|
14
|
+
end
|
15
|
+
|
16
|
+
GROUP_STRATEGY_NAME = 'group'.freeze
|
17
|
+
include Contracts
|
18
|
+
|
19
|
+
# @api public
|
20
|
+
#
|
21
|
+
# There are implicit groups that exist and are associated with a given strategy.
|
22
|
+
# This method exists to create a consistent group name.
|
23
|
+
#
|
24
|
+
# @param strategy [String] What is the scope of this identifier (i.e. a Netid, email)
|
25
|
+
# @return [Cogitate::Models::Identifier]
|
26
|
+
def self.new_for_implicit_verified_group_by_strategy(strategy:)
|
27
|
+
new(strategy: GROUP_STRATEGY_NAME, identifying_value: %(All Verified "#{strategy.to_s.downcase}" Users))
|
28
|
+
end
|
29
|
+
|
30
|
+
Contract(Cogitate::Interfaces::IdentifierInitializationInterface => Cogitate::Interfaces::IdentifierInterface)
|
31
|
+
# @api public
|
32
|
+
#
|
33
|
+
# Initialize a value object for identification
|
34
|
+
#
|
35
|
+
# @param strategy [String] What is the scope of this identifier (i.e. a Netid, email)
|
36
|
+
# @param identifying_value [String] What is the value of this identifier (i.e. hello@test.com)
|
37
|
+
def initialize(strategy:, identifying_value:)
|
38
|
+
self.strategy = strategy
|
39
|
+
self.identifying_value = identifying_value
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
# @api public
|
44
|
+
#
|
45
|
+
# Provides context for the `identifying_value`
|
46
|
+
#
|
47
|
+
# @example 'netid', 'orcid', 'email', 'twitter' are all potential strategies
|
48
|
+
#
|
49
|
+
# @return [String] one of the contexts for identity of this object
|
50
|
+
# @see #<=>
|
51
|
+
attr_reader :strategy
|
52
|
+
|
53
|
+
# @api public
|
54
|
+
#
|
55
|
+
# For the given `strategy` what is the (hopefully) unique value that can be used for identification?
|
56
|
+
#
|
57
|
+
# @example
|
58
|
+
# Given a `strategy` of "email", examples of identifying values are:
|
59
|
+
# * hello@world.com
|
60
|
+
# * test@test.com
|
61
|
+
#
|
62
|
+
# It is also possible that someone might say 'taco' is an identifying value for the email strategy.
|
63
|
+
# And that is fine.
|
64
|
+
#
|
65
|
+
# @return [String] one of the contexts for identity of this object
|
66
|
+
# @see #<=>
|
67
|
+
attr_reader :identifying_value
|
68
|
+
|
69
|
+
alias_method :name, :identifying_value
|
70
|
+
|
71
|
+
# The JSON representation of this object
|
72
|
+
#
|
73
|
+
# @return [Hash]
|
74
|
+
def as_json(*)
|
75
|
+
{ 'identifying_value' => identifying_value, 'strategy' => strategy }
|
76
|
+
end
|
77
|
+
|
78
|
+
include Comparable
|
79
|
+
|
80
|
+
Contract(Cogitate::Interfaces::IdentifierInterface => Contracts::Num)
|
81
|
+
# @api public
|
82
|
+
#
|
83
|
+
# Provide a means of sorting.
|
84
|
+
#
|
85
|
+
# @return [Integer] -1, 0, 1 as per `Comparable#<=>` interface
|
86
|
+
def <=>(other)
|
87
|
+
strategy_sort = strategy <=> other.strategy
|
88
|
+
return strategy_sort if strategy_sort != 0
|
89
|
+
identifying_value <=> other.identifying_value
|
90
|
+
end
|
91
|
+
|
92
|
+
# @api public
|
93
|
+
#
|
94
|
+
# A URL safe encoding of this object's `strategy` and `identifying_value`
|
95
|
+
#
|
96
|
+
# @return [String] a URL safe encoding of the object's identifying attributes
|
97
|
+
def encoded_id
|
98
|
+
Base64.urlsafe_encode64("#{strategy}\t#{identifying_value}")
|
99
|
+
end
|
100
|
+
|
101
|
+
alias_method :id, :encoded_id
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
# @api private
|
106
|
+
attr_writer :identifying_value
|
107
|
+
|
108
|
+
# @api private
|
109
|
+
def strategy=(value)
|
110
|
+
@strategy = value.to_s.downcase
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'cogitate/interfaces'
|
2
|
+
require 'cogitate/models/identifier'
|
3
|
+
module Cogitate
|
4
|
+
module Models
|
5
|
+
module Identifiers
|
6
|
+
# Responsible for exposing
|
7
|
+
class WithAttributeHash
|
8
|
+
include Contracts
|
9
|
+
Contract(
|
10
|
+
Contracts::KeywordArgs[identifier: ::Cogitate::Interfaces::IdentifierInterface, attributes: Contracts::HashOf[String, Any]] =>
|
11
|
+
Contracts::Any
|
12
|
+
)
|
13
|
+
def initialize(identifier:, attributes: {})
|
14
|
+
self.identifier = identifier
|
15
|
+
self.attributes = attributes
|
16
|
+
self.attributes.freeze
|
17
|
+
end
|
18
|
+
|
19
|
+
def as_json(*)
|
20
|
+
{ 'strategy' => strategy, 'identifying_value' => identifying_value }.merge(attributes)
|
21
|
+
end
|
22
|
+
|
23
|
+
extend Forwardable
|
24
|
+
include Comparable
|
25
|
+
def_delegators :identifier, *Cogitate::Models::Identifier.interface_method_names
|
26
|
+
|
27
|
+
attr_reader :attributes
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
attr_accessor :identifier
|
32
|
+
attr_writer :attributes
|
33
|
+
|
34
|
+
def method_missing(method_name, *args, &block)
|
35
|
+
attributes.fetch(method_name.to_s) { super }
|
36
|
+
end
|
37
|
+
|
38
|
+
def respond_to_missing?(method_name, *args)
|
39
|
+
attributes.key?(method_name.to_s) || super
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'contracts'
|
3
|
+
require 'cogitate/interfaces'
|
4
|
+
require 'cogitate/exceptions'
|
5
|
+
require 'cogitate/client'
|
6
|
+
|
7
|
+
module Cogitate
|
8
|
+
module Services
|
9
|
+
# A service module for extracting identifiers from an encoded payload.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
#
|
13
|
+
# encoded_string = Base64.urlsafe_encode64("netid\tmynetid")
|
14
|
+
#
|
15
|
+
#
|
16
|
+
# @see Cogitate::Services::IdentifieresDecoder.call
|
17
|
+
module IdentifiersDecoder
|
18
|
+
# Responsible for decoding a string into a collection of identifier types and identifier ids
|
19
|
+
#
|
20
|
+
# @param encoded_string [#to_s] The thing we are going to decode
|
21
|
+
# @return [Array[Hash]]
|
22
|
+
# @raise InvalidIdentifierFormat
|
23
|
+
# @raise InvalidIdentifierEncoding
|
24
|
+
#
|
25
|
+
# @api public
|
26
|
+
# @note I have chosen to not use a keyword, as I don't want to imply the "form" of object is that is being passed in.
|
27
|
+
# @todo Determine if we should return an Array of hashes or if it should be a more proper class?
|
28
|
+
extend Contracts
|
29
|
+
Contract(
|
30
|
+
String, { Contracts::KeywordArgs[identifier_builder: Contracts::Func[Cogitate::Interfaces::IdentifierBuilderInterface]] =>
|
31
|
+
Contracts::ArrayOf[Cogitate::Interfaces::IdentifierInterface] }
|
32
|
+
)
|
33
|
+
def self.call(encoded_string, identifier_builder: default_identifier_builder)
|
34
|
+
decoded_string = decode(encoded_string)
|
35
|
+
|
36
|
+
decoded_string.split("\n").each_with_object([]) do |strategy_value, object|
|
37
|
+
strategy, value = strategy_value.split("\t")
|
38
|
+
if strategy.to_s.size == 0 || value.to_s.size == 0
|
39
|
+
fail Cogitate::InvalidIdentifierFormat, decoded_string: decoded_string
|
40
|
+
end
|
41
|
+
object << identifier_builder.call(strategy: strategy, identifying_value: value)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# @api private
|
46
|
+
def self.decode(encoded_string)
|
47
|
+
Base64.urlsafe_decode64(encoded_string.to_s)
|
48
|
+
rescue ArgumentError
|
49
|
+
raise Cogitate::InvalidIdentifierEncoding, encoded_string: encoded_string
|
50
|
+
end
|
51
|
+
private_class_method :decode
|
52
|
+
|
53
|
+
# @api private
|
54
|
+
def self.default_identifier_builder
|
55
|
+
require "cogitate/models/identifier" unless defined?(Cogitate::Models::Identifier)
|
56
|
+
Cogitate::Models::Identifier.method(:new)
|
57
|
+
end
|
58
|
+
private_class_method :default_identifier_builder
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|