coarnotify 0.1.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.
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require_relative 'factory'
5
+
6
+ module Coarnotify
7
+ # Supporting classes for COAR Notify server implementations
8
+ module Server
9
+ # An object representing the response from a COAR Notify server.
10
+ #
11
+ # Server implementations should construct and return this object with the appropriate properties
12
+ # when implementing the COARNotifyServiceBinding#notification_received binding
13
+ class COARNotifyReceipt
14
+ # The status code for a created resource
15
+ CREATED = 201
16
+
17
+ # The status code for an accepted request
18
+ ACCEPTED = 202
19
+
20
+ attr_reader :status, :location
21
+
22
+ # Construct a new COARNotifyReceipt object with the status code and location URL (optional)
23
+ #
24
+ # @param status [Integer] the HTTP status code, should be one of the constants CREATED (201) or ACCEPTED (202)
25
+ # @param location [String, nil] the HTTP URI for the resource that was created (if present)
26
+ def initialize(status, location = nil)
27
+ @status = status
28
+ @location = location
29
+ end
30
+ end
31
+
32
+ # Interface for implementing a COAR Notify server binding.
33
+ #
34
+ # Server implementation should extend this class and implement the notification_received method
35
+ #
36
+ # That method will receive a NotifyPattern object, which will be one of the known types
37
+ # and should return a COARNotifyReceipt object with the appropriate status code and location URL
38
+ class COARNotifyServiceBinding
39
+ # Process the receipt of the given notification, and respond with an appropriate receipt object
40
+ #
41
+ # @param notification [Core::Notify::NotifyPattern] the notification object received
42
+ # @return [COARNotifyReceipt] the receipt object to send back to the client
43
+ def notification_received(notification)
44
+ raise NotImplementedError
45
+ end
46
+ end
47
+
48
+ # An exception class for server errors in the COAR Notify server implementation.
49
+ #
50
+ # The web layer of your server implementation should be able to intercept this from the
51
+ # COARNotifyServer#receive method and return the appropriate HTTP status code and message to the
52
+ # user in its standard way.
53
+ class COARNotifyServerError < StandardError
54
+ attr_reader :status, :message
55
+
56
+ # Construct a new COARNotifyServerError with the given status code and message
57
+ #
58
+ # @param status [Integer] HTTP Status code to respond to the client with
59
+ # @param msg [String] Message to send back to the client
60
+ def initialize(status, msg)
61
+ @status = status
62
+ @message = msg
63
+ super(msg)
64
+ end
65
+ end
66
+
67
+ # The main entrypoint to the COAR Notify server implementation.
68
+ #
69
+ # The web layer of your application should pass the json/raw payload of any incoming notification to the
70
+ # receive method, which will parse the payload and pass it to the COARNotifyServiceBinding#notification_received
71
+ # method of your service implementation
72
+ #
73
+ # This object should be constructed with your service implementation passed to it, for example:
74
+ #
75
+ # server = COARNotifyServer.new(MyServiceBinding.new)
76
+ # begin
77
+ # response = server.receive(request.body)
78
+ # # return response as JSON
79
+ # rescue COARNotifyServerError => e
80
+ # # return error with status e.status and message e.message
81
+ # end
82
+ class COARNotifyServer
83
+ # Construct a new COARNotifyServer with the given service implementation
84
+ #
85
+ # @param service_impl [COARNotifyServiceBinding] Your service implementation
86
+ def initialize(service_impl)
87
+ @service_impl = service_impl
88
+ end
89
+
90
+ # Receive an incoming notification as JSON, parse and validate (optional) and then pass to the
91
+ # service implementation
92
+ #
93
+ # @param raw [Hash, String] The JSON representation of the data, either as a string or a hash
94
+ # @param validate [Boolean] Whether to validate the notification before passing to the service implementation
95
+ # @return [COARNotifyReceipt] The COARNotifyReceipt response from the service implementation
96
+ def receive(raw, validate: true)
97
+ raw = JSON.parse(raw) if raw.is_a?(String)
98
+
99
+ obj = Factory::COARNotifyFactory.get_by_object(raw, validate_stream_on_construct: false)
100
+ if validate
101
+ begin
102
+ obj.validate
103
+ rescue ValidationError => e
104
+ raise COARNotifyServerError.new(400, "Invalid notification")
105
+ end
106
+ end
107
+
108
+ @service_impl.notification_received(obj)
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'set'
5
+
6
+ module Coarnotify
7
+ # This module provides a set of validation functions that can be used to validate properties on objects.
8
+ # It also contains a Validator class which is used to wrap the protocol-wide validation rules which
9
+ # are shared across all objects.
10
+ module Validate
11
+ REQUIRED_MESSAGE = "`%s` is a required field"
12
+
13
+ # A wrapper around a set of validation rules which can be used to select the appropriate validator
14
+ # in a given context.
15
+ #
16
+ # The validation rules are structured as follows:
17
+ #
18
+ # {
19
+ # "<property>" => {
20
+ # "default" => default_validator_function,
21
+ # "context" => {
22
+ # "<context>" => {
23
+ # "default" => default_validator_function
24
+ # }
25
+ # }
26
+ # }
27
+ # }
28
+ #
29
+ # Here the <property> key is the name of the property being validated, which may be a string (the property name)
30
+ # or an array of strings (the property name and the namespace for the property name).
31
+ #
32
+ # If a context is provided, then if the top level property is being validated, and it appears inside a field
33
+ # present in the context then the default validator at the top level is overridden by the default validator
34
+ # in the context.
35
+ class Validator
36
+ attr_reader :rules
37
+
38
+ # Create a new validator with the given rules
39
+ #
40
+ # @param rules [Hash] The rules to use for validation
41
+ def initialize(rules)
42
+ @rules = rules
43
+ end
44
+
45
+ # Get the validation function for the given property in the given context
46
+ #
47
+ # @param property [String, Array] the property to get the validation function for
48
+ # @param context [String, Array] the context in which the property is being validated
49
+ # @return [Proc] a function which can be used to validate the property
50
+ def get(property, context = nil)
51
+ default = @rules.dig(property, "default")
52
+ if context
53
+ # FIXME: down the line this might need to become recursive
54
+ specific = @rules.dig(property, "context", context, "default")
55
+ return specific if specific
56
+ end
57
+ default
58
+ end
59
+
60
+ # Add additional rules to this validator
61
+ #
62
+ # @param rules [Hash] additional rules to merge
63
+ def add_rules(rules)
64
+ @rules = merge_dicts_recursive(@rules, rules)
65
+ end
66
+
67
+ private
68
+
69
+ def merge_dicts_recursive(dict1, dict2)
70
+ merged = dict1.dup
71
+ dict2.each do |key, value|
72
+ if merged.key?(key) && merged[key].is_a?(Hash) && value.is_a?(Hash)
73
+ merged[key] = merge_dicts_recursive(merged[key], value)
74
+ else
75
+ merged[key] = value
76
+ end
77
+ end
78
+ merged
79
+ end
80
+ end
81
+
82
+ # URI validation regular expressions
83
+ URI_RE = /^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/
84
+ SCHEME_RE = /^[a-zA-Z][a-zA-Z0-9+\-.]*$/
85
+ IPV6_RE = /(?:^|(?<=\s))\[{0,1}(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\]{0,1}(?=\s|$)/
86
+
87
+ HOSTPORT_RE = /^(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|localhost|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|(?:^|(?<=\s))(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))(?=\s|$))(?::\d+)?$/i
88
+
89
+ MARK = "\\-_.!~*'()"
90
+ UNRESERVED = "a-zA-Z0-9#{MARK}"
91
+ PCHARS = "#{UNRESERVED}:@&=+$,%/;"
92
+ PATH_RE = /^#{Regexp.escape('/')}?[#{PCHARS}]*$/
93
+
94
+ RESERVED = ";/?:@&=+$,"
95
+ URIC = "#{RESERVED}#{UNRESERVED}%"
96
+ FREE_RE = /^[#{URIC}]+$/
97
+
98
+ USERINFO_RE = /^[#{UNRESERVED}%;:&=+$,]*$/
99
+
100
+ # Validate that the given string is an absolute URI
101
+ #
102
+ # @param obj [Object] The Notify object to which the property being validated belongs
103
+ # @param uri [String] The string that claims to be an absolute URI
104
+ # @return [Boolean] true if the URI is valid, otherwise ArgumentError is raised
105
+ def self.absolute_uri(obj, uri)
106
+ m = URI_RE.match(uri)
107
+ raise ArgumentError, "Invalid URI" unless m
108
+
109
+ # URI must be absolute, so requires a scheme
110
+ raise ArgumentError, "URI requires a scheme (this may be a relative rather than absolute URI)" unless m[2]
111
+
112
+ scheme = m[2]
113
+ authority = m[4]
114
+ path = m[5]
115
+ query = m[7]
116
+ fragment = m[9]
117
+
118
+ # scheme must be alpha followed by alphanum or +, -, or .
119
+ if scheme
120
+ raise ArgumentError, "Invalid URI scheme `#{scheme}`" unless SCHEME_RE.match?(scheme)
121
+ end
122
+
123
+ if authority
124
+ userinfo = nil
125
+ hostport = authority
126
+ if authority.include?("@")
127
+ userinfo, hostport = authority.split("@", 2)
128
+ end
129
+ if userinfo
130
+ raise ArgumentError, "Invalid URI authority `#{authority}`" unless USERINFO_RE.match?(userinfo)
131
+ end
132
+ # determine if the domain is ipv6
133
+ if hostport.start_with?("[") # ipv6 with an optional port
134
+ port_separator = hostport.rindex("]:")
135
+ port = nil
136
+ if port_separator
137
+ port = hostport[port_separator+2..-1]
138
+ host = hostport[1...port_separator]
139
+ else
140
+ host = hostport[1..-2]
141
+ end
142
+ raise ArgumentError, "Invalid URI authority `#{authority}`" unless IPV6_RE.match?(host)
143
+ if port
144
+ begin
145
+ Integer(port)
146
+ rescue ArgumentError
147
+ raise ArgumentError, "Invalid URI port `#{port}`"
148
+ end
149
+ end
150
+ else
151
+ raise ArgumentError, "Invalid URI authority `#{authority}`" unless HOSTPORT_RE.match?(hostport)
152
+ end
153
+ end
154
+
155
+ if path
156
+ raise ArgumentError, "Invalid URI path `#{path}`" unless PATH_RE.match?(path)
157
+ end
158
+
159
+ if query
160
+ raise ArgumentError, "Invalid URI query `#{query}`" unless FREE_RE.match?(query)
161
+ end
162
+
163
+ if fragment
164
+ raise ArgumentError, "Invalid URI fragment `#{fragment}`" unless FREE_RE.match?(fragment)
165
+ end
166
+
167
+ true
168
+ end
169
+
170
+ # Validate that the given string is an absolute HTTP URI (i.e. a URL)
171
+ #
172
+ # @param obj [Object] The Notify object to which the property being validated belongs
173
+ # @param url [String] The string that claims to be an HTTP URI
174
+ # @return [Boolean] true if the URI is valid, otherwise ArgumentError is raised
175
+ def self.url(obj, url)
176
+ absolute_uri(obj, url)
177
+ o = URI.parse(url)
178
+ raise ArgumentError, "URL scheme must be http or https" unless %w[http https].include?(o.scheme)
179
+ raise ArgumentError, "Does not appear to be a valid URL" if o.host.nil? || o.host.empty?
180
+ true
181
+ end
182
+
183
+ # Closure that returns a validation function that checks that the value is one of the given values
184
+ #
185
+ # @param values [Array<String>] The list of values to choose from
186
+ # @return [Proc] a validation function
187
+ def self.one_of(values)
188
+ proc do |obj, x|
189
+ unless values.include?(x)
190
+ raise ArgumentError, "`#{x}` is not one of the valid values: #{values}"
191
+ end
192
+ true
193
+ end
194
+ end
195
+
196
+ # Closure that returns a validation function that checks that a list of values contains at least one
197
+ # of the given values
198
+ #
199
+ # @param values [Array<String>] The list of values to choose from
200
+ # @return [Proc] a validation function
201
+ def self.at_least_one_of(values)
202
+ proc do |obj, x|
203
+ x = [x] unless x.is_a?(Array)
204
+
205
+ found = x.any? { |entry| values.include?(entry) }
206
+
207
+ unless found
208
+ # if we don't find one of the document values in the list of "at least one of" values,
209
+ # raise an exception
210
+ raise ArgumentError, "`#{x}` is not one of the valid values: #{values}"
211
+ end
212
+
213
+ true
214
+ end
215
+ end
216
+
217
+ # Closure that returns a validation function that checks the provided values contain the required value
218
+ #
219
+ # @param value [String, Array<String>] The value(s) that must be present
220
+ # @return [Proc] a validation function
221
+ def self.contains(value)
222
+ values = value.is_a?(Array) ? value : [value]
223
+ values_set = values.to_set
224
+
225
+ proc do |obj, x|
226
+ x = [x] unless x.is_a?(Array)
227
+ x_set = x.to_set
228
+
229
+ intersection = x_set & values_set
230
+ unless intersection == values_set
231
+ raise ArgumentError, "`#{x}` does not contain the required value(s): #{values}"
232
+ end
233
+ true
234
+ end
235
+ end
236
+
237
+ # Validate that the given value is of the correct type for the object
238
+ #
239
+ # @param obj [Object] the notify object being validated
240
+ # @param value [String, Array<String>] the type being validated
241
+ # @return [Boolean] true if the type is valid, otherwise ArgumentError is raised
242
+ def self.type_checker(obj, value)
243
+ if obj.respond_to?(:allowed_types)
244
+ allowed = obj.allowed_types
245
+ return true if allowed.empty?
246
+ validator = one_of(allowed)
247
+ validator.call(obj, value)
248
+ elsif obj.respond_to?(:type_constant)
249
+ ty = obj.type_constant
250
+ validator = contains(ty)
251
+ validator.call(obj, value)
252
+ end
253
+ true
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/coarnotify/version.rb
4
+ module Coarnotify
5
+ # Version of the coarnotifyrb gem
6
+ VERSION = "0.1.0"
7
+ end
8
+
data/lib/coarnotify.rb ADDED
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ # This is the base of the coarnotifyrb module.
6
+ #
7
+ # In here you will find
8
+ # a full set of model objects for all the Notify Patterns documented in
9
+ # https://coar-notify.net/specification/1.0.1/
10
+ #
11
+ # You will also find a client library that will allow you to send notifications
12
+ # to an inbox, and a server library that will allow you to write a service
13
+ # binding to your own systems to receive notifications via an inbox.
14
+ #
15
+ # There are also unit tests demonstrating the various features of the system,
16
+ # integration tests which can be run against a remote inbox, and a
17
+ # stand-alone inbox you can use for local testing.
18
+
19
+ require_relative 'coarnotify/version'
20
+ require_relative 'coarnotify/exceptions'
21
+ require_relative 'coarnotify/validate'
22
+ require_relative 'coarnotify/core/activity_streams2'
23
+ require_relative 'coarnotify/core/notify'
24
+ require_relative 'coarnotify/http'
25
+ require_relative 'coarnotify/patterns/accept'
26
+ require_relative 'coarnotify/patterns/announce_endorsement'
27
+ require_relative 'coarnotify/patterns/announce_relationship'
28
+ require_relative 'coarnotify/patterns/announce_review'
29
+ require_relative 'coarnotify/patterns/announce_service_result'
30
+ require_relative 'coarnotify/patterns/reject'
31
+ require_relative 'coarnotify/patterns/request_endorsement'
32
+ require_relative 'coarnotify/patterns/request_review'
33
+ require_relative 'coarnotify/patterns/tentatively_accept'
34
+ require_relative 'coarnotify/patterns/tentatively_reject'
35
+ require_relative 'coarnotify/patterns/undo_offer'
36
+ require_relative 'coarnotify/patterns/unprocessable_notification'
37
+ require_relative 'coarnotify/factory'
38
+ require_relative 'coarnotify/client'
39
+ require_relative 'coarnotify/server'
40
+
41
+ # Main module for the COAR Notify Ruby implementation
42
+ module Coarnotify
43
+ # Convenience method to create a new COAR Notify client
44
+ #
45
+ # @param inbox_url [String, nil] HTTP URI of the inbox to communicate with by default
46
+ # @param http_layer [Http::HttpLayer, nil] An implementation of the HttpLayer interface
47
+ # @return [Client::COARNotifyClient] a new client instance
48
+ def self.client(inbox_url: nil, http_layer: nil)
49
+ Client::COARNotifyClient.new(inbox_url: inbox_url, http_layer: http_layer)
50
+ end
51
+
52
+ # Convenience method to create a new COAR Notify server
53
+ #
54
+ # @param service_impl [Server::COARNotifyServiceBinding] Your service implementation
55
+ # @return [Server::COARNotifyServer] a new server instance
56
+ def self.server(service_impl)
57
+ Server::COARNotifyServer.new(service_impl)
58
+ end
59
+
60
+ # Convenience method to create a pattern from a hash
61
+ #
62
+ # @param data [Hash] The raw stream data to parse and instantiate around
63
+ # @param options [Hash] any options to pass to the object constructor
64
+ # @return [Core::Notify::NotifyPattern] A NotifyPattern of the correct type
65
+ def self.from_hash(data, **options)
66
+ Factory::COARNotifyFactory.get_by_object(data, **options)
67
+ end
68
+
69
+ # Convenience method to create a pattern from JSON
70
+ #
71
+ # @param json [String] The JSON string to parse and instantiate around
72
+ # @param options [Hash] any options to pass to the object constructor
73
+ # @return [Core::Notify::NotifyPattern] A NotifyPattern of the correct type
74
+ def self.from_json(json, **options)
75
+ data = JSON.parse(json)
76
+ from_hash(data, **options)
77
+ end
78
+ end
metadata ADDED
@@ -0,0 +1,121 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: coarnotify
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Cottage Labs
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-12-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '13.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '13.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
69
+ description: COAR Notify Common Library
70
+ email:
71
+ - us@cottagelabs.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - lib/coarnotify.rb
77
+ - lib/coarnotify/client.rb
78
+ - lib/coarnotify/core/activity_streams2.rb
79
+ - lib/coarnotify/core/notify.rb
80
+ - lib/coarnotify/exceptions.rb
81
+ - lib/coarnotify/factory.rb
82
+ - lib/coarnotify/http.rb
83
+ - lib/coarnotify/patterns/accept.rb
84
+ - lib/coarnotify/patterns/announce_endorsement.rb
85
+ - lib/coarnotify/patterns/announce_relationship.rb
86
+ - lib/coarnotify/patterns/announce_review.rb
87
+ - lib/coarnotify/patterns/announce_service_result.rb
88
+ - lib/coarnotify/patterns/reject.rb
89
+ - lib/coarnotify/patterns/request_endorsement.rb
90
+ - lib/coarnotify/patterns/request_review.rb
91
+ - lib/coarnotify/patterns/tentatively_accept.rb
92
+ - lib/coarnotify/patterns/tentatively_reject.rb
93
+ - lib/coarnotify/patterns/undo_offer.rb
94
+ - lib/coarnotify/patterns/unprocessable_notification.rb
95
+ - lib/coarnotify/server.rb
96
+ - lib/coarnotify/validate.rb
97
+ - lib/coarnotify/version.rb
98
+ homepage:
99
+ licenses:
100
+ - MIT
101
+ metadata: {}
102
+ post_install_message:
103
+ rdoc_options: []
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ required_rubygems_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ requirements: []
117
+ rubygems_version: 3.4.20
118
+ signing_key:
119
+ specification_version: 4
120
+ summary: COAR Notify Common Library
121
+ test_files: []