cogitate 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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