cogitate 0.0.1 → 0.0.2

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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +69 -15
  3. data/lib/cogitate.rb +18 -2
  4. data/lib/cogitate/README.md +8 -0
  5. data/lib/cogitate/client.rb +97 -0
  6. data/lib/cogitate/client/agent_builder.rb +86 -0
  7. data/lib/cogitate/client/data_to_object_coercer.rb +38 -0
  8. data/lib/cogitate/client/exceptions.rb +10 -0
  9. data/lib/cogitate/client/identifier_builder.rb +26 -0
  10. data/lib/cogitate/client/request.rb +56 -0
  11. data/lib/cogitate/client/response_parsers.rb +19 -0
  12. data/lib/cogitate/client/response_parsers/agents_with_detailed_identifiers_extractor.rb +15 -0
  13. data/lib/cogitate/client/response_parsers/agents_without_group_membership_extractor.rb +23 -0
  14. data/lib/cogitate/client/response_parsers/basic_extractor.rb +14 -0
  15. data/lib/cogitate/client/response_parsers/email_extractor.rb +17 -0
  16. data/lib/cogitate/client/retrieve_agent_from_ticket.rb +43 -0
  17. data/lib/cogitate/client/ticket_to_token_coercer.rb +35 -0
  18. data/lib/cogitate/client/token_to_object_coercer.rb +38 -0
  19. data/lib/cogitate/configuration.rb +99 -0
  20. data/lib/cogitate/exceptions.rb +25 -0
  21. data/lib/cogitate/generators/install_generator.rb +10 -0
  22. data/lib/cogitate/generators/templates/cogitate_initializer.rb.erb +17 -0
  23. data/lib/cogitate/interfaces.rb +40 -0
  24. data/lib/cogitate/models.rb +6 -0
  25. data/lib/cogitate/models/agent.rb +143 -0
  26. data/lib/cogitate/models/agent/serializer.rb +75 -0
  27. data/lib/cogitate/models/agent/with_token.rb +29 -0
  28. data/lib/cogitate/models/identifier.rb +114 -0
  29. data/lib/cogitate/models/identifiers.rb +7 -0
  30. data/lib/cogitate/models/identifiers/with_attribute_hash.rb +44 -0
  31. data/lib/cogitate/railtie.rb +8 -0
  32. data/lib/cogitate/services/identifiers_decoder.rb +61 -0
  33. data/lib/cogitate/services/tokenizer.rb +80 -0
  34. metadata +105 -5
@@ -0,0 +1,6 @@
1
+ module Cogitate
2
+ # The data structure and model definitions of Cogitate objects.
3
+ # Found within this module are models that are shared concepts between Client and Server.
4
+ module Models
5
+ end
6
+ end
@@ -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,7 @@
1
+ module Cogitate
2
+ module Models
3
+ # Establishing namespace
4
+ module Identifiers
5
+ end
6
+ end
7
+ 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,8 @@
1
+ module Cogitate
2
+ # :nodoc:
3
+ class Railtie < Rails::Railtie
4
+ generators do
5
+ require 'cogitate/generators/install_generator'
6
+ end
7
+ end
8
+ 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