sferik 0.0.1

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 (71) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +5 -0
  3. data/CHANGELOG.md +72 -0
  4. data/LICENSE.md +16 -0
  5. data/README.md +517 -0
  6. data/exe/sferik +6 -0
  7. data/lib/sferik/api/code_endpoints.rb +37 -0
  8. data/lib/sferik/api/profile_endpoints.rb +113 -0
  9. data/lib/sferik/api/resume_endpoints.rb +41 -0
  10. data/lib/sferik/api/site_endpoints.rb +247 -0
  11. data/lib/sferik/api/talk_endpoints.rb +45 -0
  12. data/lib/sferik/api.rb +23 -0
  13. data/lib/sferik/block.rb +25 -0
  14. data/lib/sferik/body.rb +54 -0
  15. data/lib/sferik/cache.rb +361 -0
  16. data/lib/sferik/cli.rb +369 -0
  17. data/lib/sferik/client.rb +453 -0
  18. data/lib/sferik/collection.rb +82 -0
  19. data/lib/sferik/configuration.rb +129 -0
  20. data/lib/sferik/connections.rb +151 -0
  21. data/lib/sferik/contributions.rb +86 -0
  22. data/lib/sferik/day.rb +35 -0
  23. data/lib/sferik/dependency.rb +26 -0
  24. data/lib/sferik/deployment.rb +36 -0
  25. data/lib/sferik/errors.rb +234 -0
  26. data/lib/sferik/figure.rb +83 -0
  27. data/lib/sferik/finger.rb +74 -0
  28. data/lib/sferik/freshness.rb +54 -0
  29. data/lib/sferik/home/pages.rb +27 -0
  30. data/lib/sferik/home/profile.rb +51 -0
  31. data/lib/sferik/home/section.rb +27 -0
  32. data/lib/sferik/home.rb +38 -0
  33. data/lib/sferik/json_parsing.rb +28 -0
  34. data/lib/sferik/name_change.rb +49 -0
  35. data/lib/sferik/place.rb +33 -0
  36. data/lib/sferik/podcast.rb +41 -0
  37. data/lib/sferik/project.rb +49 -0
  38. data/lib/sferik/projects.rb +138 -0
  39. data/lib/sferik/push.rb +33 -0
  40. data/lib/sferik/resource.rb +373 -0
  41. data/lib/sferik/resume/award.rb +43 -0
  42. data/lib/sferik/resume/basics.rb +77 -0
  43. data/lib/sferik/resume/education.rb +59 -0
  44. data/lib/sferik/resume/location.rb +35 -0
  45. data/lib/sferik/resume/meta.rb +35 -0
  46. data/lib/sferik/resume/patent.rb +43 -0
  47. data/lib/sferik/resume/profile.rb +35 -0
  48. data/lib/sferik/resume/project.rb +27 -0
  49. data/lib/sferik/resume/skill.rb +27 -0
  50. data/lib/sferik/resume/speaking.rb +19 -0
  51. data/lib/sferik/resume/volunteer.rb +59 -0
  52. data/lib/sferik/resume/work.rb +51 -0
  53. data/lib/sferik/resume.rb +125 -0
  54. data/lib/sferik/session.rb +41 -0
  55. data/lib/sferik/social_profile.rb +49 -0
  56. data/lib/sferik/status/github.rb +38 -0
  57. data/lib/sferik/status/loaded.rb +48 -0
  58. data/lib/sferik/status.rb +33 -0
  59. data/lib/sferik/talk.rb +73 -0
  60. data/lib/sferik/talks.rb +79 -0
  61. data/lib/sferik/validation.rb +119 -0
  62. data/lib/sferik/version.rb +7 -0
  63. data/lib/sferik/web_finger/link.rb +46 -0
  64. data/lib/sferik/web_finger.rb +43 -0
  65. data/lib/sferik/who.rb +53 -0
  66. data/lib/sferik/whoami.rb +36 -0
  67. data/lib/sferik/wrapping.rb +114 -0
  68. data/lib/sferik.rb +246 -0
  69. data/sig/manifest.yaml +13 -0
  70. data/sig/sferik.rbs +825 -0
  71. metadata +118 -0
@@ -0,0 +1,361 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require_relative "freshness"
5
+
6
+ module Sferik
7
+ # Keeps the responses to a client's GET requests, and asks again only for what may have changed
8
+ #
9
+ # It stands where the client's {Connections} do, and makes its requests over them. A response says how long it's
10
+ # good for (Cache-Control: max-age), and for that long the same URL asked for as the same media type is answered
11
+ # with it, and no request is made. After that, the request says which version is kept (If-None-Match, with the
12
+ # response's ETag), and the server answers 304 Not Modified, without a body, if that's still the one. A response
13
+ # that says not to keep it (no-store) isn't kept, and one that says to check each time (no-cache) is checked each
14
+ # time. Only a 200 is kept, and what's kept stays kept when the server answers with an error of its own (a 5xx),
15
+ # which says nothing of whether it has changed: the next request asks again.
16
+ #
17
+ # A response that has already been kept somewhere on its way, as one from Cloudflare's cache has, says for how long
18
+ # (Age), and is good for that much less. That cache answers with what it has, when that's no longer good, while it
19
+ # builds another for whoever asks next. So each request this cache makes says not to be answered that way
20
+ # (Cache-Control: no-cache), which the site takes to mean that it should wait for the new one: what's still good
21
+ # there is its answer all the same. And a cache that's told to (stale) answers with a response that's no longer
22
+ # good, when the server can't be asked whether it has changed, or answers with an error of its own.
23
+ #
24
+ # Threads that ask for the same thing at once, when it isn't kept or is no longer good, make one request between
25
+ # them: the first asks, and the rest wait for its answer, which is theirs too. If it gets none, each asks for itself.
26
+ #
27
+ # A hundred responses are kept at most: one more, and the one that was asked of the server longest ago is forgotten.
28
+ #
29
+ # What a client makes of a response (the JSON parsed, and a resource built of it) is kept with the response, and
30
+ # made once: see {#made}.
31
+ #
32
+ # @api private
33
+ class Cache
34
+ # A response that's kept, with its ETag, if it has one, when on the clock it's good until, and what was made of it
35
+ #
36
+ # @!attribute [r] response
37
+ # The response
38
+ # @api private
39
+ # @return [Net::HTTPResponse] the response
40
+ # @!attribute [r] etag
41
+ # The response's ETag
42
+ # @api private
43
+ # @return [String, nil] the ETag, or nil if the response has none
44
+ # @!attribute [r] expires
45
+ # When the response is good until
46
+ # @api private
47
+ # @return [Numeric] the time on the cache's clock, in seconds
48
+ # @!attribute [r] made
49
+ # What was made of the response
50
+ # @api private
51
+ # @return [Object, nil] what {Cache#made}'s block returned, or nil if nothing has been made of it yet
52
+ Entry = Data.define(:response, :etag, :expires, :made)
53
+ private_constant :Entry
54
+
55
+ # A request on its way, whose answer is for every thread that asks for the same thing before it comes
56
+ #
57
+ # @api private
58
+ class Flight
59
+ # Initialize a flight, which hasn't landed
60
+ #
61
+ # @api private
62
+ # @return [Flight] the flight
63
+ def initialize
64
+ @landed = Queue.new
65
+ end
66
+
67
+ # Make the request, and say what it got
68
+ #
69
+ # That's said to the threads that are waiting, and to any that ask later.
70
+ #
71
+ # @api private
72
+ # @yield the request to make
73
+ # @yieldreturn [Net::HTTPResponse] the response
74
+ # @return [Net::HTTPResponse] the response
75
+ def fly
76
+ @response = yield
77
+ ensure
78
+ @landed.close
79
+ end
80
+
81
+ # Wait for the request to get its answer, and return it
82
+ #
83
+ # @api private
84
+ # @return [Net::HTTPResponse, nil] the response, or nil if the request got none
85
+ def response
86
+ @landed.deq
87
+ @response
88
+ end
89
+ end
90
+ private_constant :Flight
91
+
92
+ # The time, in seconds, on a clock that only goes forward
93
+ CLOCK = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
94
+ private_constant :CLOCK
95
+
96
+ # The most responses to keep: far more than the API has, but an end to what a client keeps of URLs that are made
97
+ # up as it goes, each with a query of its own
98
+ LIMIT = 100
99
+ private_constant :LIMIT
100
+
101
+ # Initialize a cache
102
+ #
103
+ # @api private
104
+ # @param connections [Connections, Cache] what to make requests over
105
+ # @param clock [#call] what tells the time, in seconds
106
+ # @param entries [Hash{Array => Entry}] the responses kept, by URL and media type
107
+ # @param lock [Mutex] what's held to read or change them, and the requests on their way
108
+ # @param stale [Boolean] whether to answer with a response that's no longer good, when the server can't be reached,
109
+ # or answers with an error of its own
110
+ # @return [Cache] the cache
111
+ def initialize(connections, clock = CLOCK, entries = {}, lock = Mutex.new, stale: false)
112
+ @connections = connections
113
+ @clock = clock
114
+ @entries = entries
115
+ @lock = lock
116
+ @flights = {} #: Hash[key, Flight]
117
+ @stale = stale
118
+ end
119
+
120
+ # Yield a cache of the same responses over connections that are kept open
121
+ #
122
+ # It's this cache in all but its connections: what one keeps, the other has, and a request of one's that's on its
123
+ # way is one the other waits for.
124
+ #
125
+ # @api private
126
+ # @yield [cache] the requests to make
127
+ # @yieldparam cache [Cache] a cache whose connections are kept open
128
+ # @yieldreturn [Object] anything
129
+ # @return [Object] what the block returns
130
+ def keeping
131
+ @connections.keeping { |kept| yield dup.over(kept) }
132
+ end
133
+
134
+ # Close the connections requests are made over, and keep the responses
135
+ #
136
+ # @api private
137
+ # @return [nil]
138
+ def close = @connections.close
139
+
140
+ # The connections requests are made over, which keep no responses
141
+ #
142
+ # They're what another cache is built over, so that it isn't over this one.
143
+ #
144
+ # @api private
145
+ # @return [Connections] the connections
146
+ def uncached = @connections
147
+
148
+ # Send a request, unless it's a GET whose response is kept and still good
149
+ #
150
+ # @api private
151
+ # @param request [Net::HTTPRequest] the request, to a URL
152
+ # @return [Net::HTTPResponse] the response, which may be one that was kept, or the server's error if the cache isn't
153
+ # to answer with what's kept instead
154
+ # @raise [NetworkError] if the server can't be reached, or its response can't be read, and there's no response
155
+ # kept to answer with instead, or the cache isn't to
156
+ def request(request)
157
+ return @connections.request(request) unless request.instance_of?(Net::HTTP::Get)
158
+
159
+ key = [request.uri, request.fetch("accept")] #: key
160
+ entry = @lock.synchronize { @entries[key] }
161
+ (entry && @clock.call < entry.expires) ? entry.response : share(key) { renew(key, entry, request) }
162
+ end
163
+
164
+ # What a block makes of a response, which is made once of a response that's kept
165
+ #
166
+ # The cache answers with a response it keeps again and again, and what's made of it would be the same each time:
167
+ # so that's kept with it, and the block isn't called again until another response is. It should make something
168
+ # that can't be changed, since every caller gets the same one.
169
+ #
170
+ # @api private
171
+ # @param uri [URI::HTTP] the URL that was asked for
172
+ # @param accept [String] the media type it was asked for as
173
+ # @param response [Net::HTTPResponse] the response that came
174
+ # @yield what to make of the response
175
+ # @yieldreturn [Object] what's made of it, which isn't nil or false
176
+ # @return [Object] what the block returned: this time, or the first time for this response
177
+ def made(uri, accept, response)
178
+ key = [uri, accept] #: key
179
+ entry = @lock.synchronize { @entries[key] }
180
+ return yield unless entry && entry.response.equal?(response)
181
+
182
+ entry.made || remember(key, entry, yield)
183
+ end
184
+
185
+ protected
186
+
187
+ # Make this cache's requests over other connections
188
+ #
189
+ # @api private
190
+ # @param connections [Connections, Cache] the connections
191
+ # @return [Cache] the cache itself
192
+ def over(connections)
193
+ @connections = connections
194
+ self
195
+ end
196
+
197
+ private
198
+
199
+ # Make a request, unless one for the same thing is on its way
200
+ #
201
+ # Then wait for that one, and answer with what it gets. If it gets no answer, the request is made after all.
202
+ #
203
+ # @api private
204
+ # @param key [Array] the URL and the media type asked for
205
+ # @yield the request to make
206
+ # @yieldreturn [Net::HTTPResponse] the response
207
+ # @return [Net::HTTPResponse] the response: this request's, or the one that was on its way
208
+ def share(key, &ask)
209
+ mine = Flight.new
210
+ flight = @lock.synchronize { @flights[key] ||= mine }
211
+ return flight.response || ask.call unless flight.equal?(mine)
212
+
213
+ lead(key, mine, &ask)
214
+ end
215
+
216
+ # Make a request that other threads may be waiting for, and tell them what it gets
217
+ #
218
+ # It's no longer on its way before they're told, so that a thread that asks after it has landed starts another.
219
+ #
220
+ # @api private
221
+ # @param key [Array] the URL and the media type asked for
222
+ # @param flight [Flight] the request on its way
223
+ # @yield the request to make
224
+ # @yieldreturn [Net::HTTPResponse] the response
225
+ # @return [Net::HTTPResponse] the response
226
+ def lead(key, flight, &ask)
227
+ flight.fly do
228
+ ask.call
229
+ ensure
230
+ @lock.synchronize { @flights.delete(key) }
231
+ end
232
+ end
233
+
234
+ # Send a GET request, or answer with what's kept if the server can't be reached
235
+ #
236
+ # What comes back is kept. What's kept and no longer good is the answer only for a cache that's told to (stale).
237
+ #
238
+ # @api private
239
+ # @param key [Array] the URL and the media type asked for
240
+ # @param entry [Entry, nil] the response kept, which is no longer good, if there is one
241
+ # @param request [Net::HTTP::Get] the request
242
+ # @return [Net::HTTPResponse] the response: the one kept, if the server says it hasn't changed, or can't be reached
243
+ # and the cache answers with what's no longer good
244
+ # @raise [NetworkError] if the server can't be reached, or its response can't be read, and there's no response
245
+ # kept to answer with instead, or the cache isn't to
246
+ def renew(key, entry, request)
247
+ ask(key, entry, request)
248
+ rescue NetworkError
249
+ raise unless entry && @stale
250
+
251
+ entry.response
252
+ end
253
+
254
+ # Send a GET request, and keep what comes back
255
+ #
256
+ # The request says which version of its response is kept, so that the server sends a body only for another. And
257
+ # it says not to be answered from a cache (Cache-Control: no-cache): the one in front of the site answers with
258
+ # what it has kept past what it's good for, otherwise, which would be no longer good when it came, and to be
259
+ # asked for again. What that cache has that's still good is its answer either way. An error of the server's own
260
+ # (a 5xx) leaves what's kept as it is, to be asked after again.
261
+ #
262
+ # @api private
263
+ # @param key [Array] the URL and the media type asked for
264
+ # @param entry [Entry, nil] the response kept, which is no longer good, if there is one
265
+ # @param request [Net::HTTP::Get] the request
266
+ # @return [Net::HTTPResponse] the response: the one kept, if the server says it hasn't changed, or answers with
267
+ # an error of its own and the cache answers with what's no longer good
268
+ def ask(key, entry, request)
269
+ request["if-none-match"] = entry.etag if entry # no header, for a response that has no ETag
270
+ request["cache-control"] = "no-cache"
271
+ response = @connections.request(request)
272
+ return spared(entry, response) if entry && response.is_a?(Net::HTTPServerError)
273
+
274
+ renewed(key, entry, response)
275
+ end
276
+
277
+ # Keep what the server answers with, or go on keeping what it says hasn't changed
278
+ #
279
+ # @api private
280
+ # @param key [Array] the URL and the media type asked for
281
+ # @param entry [Entry, nil] the response kept, which is no longer good, if there is one
282
+ # @param response [Net::HTTPResponse] the server's answer
283
+ # @return [Net::HTTPResponse] the response: the one kept, if the server says it hasn't changed
284
+ def renewed(key, entry, response)
285
+ kept = entry if response.instance_of?(Net::HTTPNotModified)
286
+ latest = kept ? kept.with(expires: expiry(response)) : Entry.new(response, response["etag"], expiry(response), nil)
287
+ keep(key, latest, response)
288
+ latest.response
289
+ end
290
+
291
+ # Keep what was made of a response with it, if it's still the one that's kept
292
+ #
293
+ # @api private
294
+ # @param key [Array] the URL and the media type asked for
295
+ # @param entry [Entry] the response kept, when it was made
296
+ # @param made [Object] what was made of it
297
+ # @return [Object] what was made of it
298
+ def remember(key, entry, made)
299
+ @lock.synchronize { @entries[key] = entry.with(made:) if @entries[key].equal?(entry) }
300
+ made
301
+ end
302
+
303
+ # What to answer with when the server fails, and a response is kept
304
+ #
305
+ # The server fails when it answers with an error of its own (a 5xx).
306
+ #
307
+ # @api private
308
+ # @param entry [Entry] the response kept, which is no longer good
309
+ # @param response [Net::HTTPResponse] the server's error
310
+ # @return [Net::HTTPResponse] the response kept, for a cache that answers with what's no longer good (stale), or
311
+ # else the error
312
+ def spared(entry, response)
313
+ @stale ? entry.response : response
314
+ end
315
+
316
+ # Keep a response, or forget the one kept
317
+ #
318
+ # Only a 200 is kept, and not one that says not to keep it: anything else leaves nothing kept. (An error of the
319
+ # server's own, with a response kept, never gets here.) One that's kept is the latest, whether or not one was
320
+ # kept for the same thing before.
321
+ #
322
+ # @api private
323
+ # @param key [Array] the URL and the media type asked for
324
+ # @param entry [Entry] the response to keep, with when it's good until
325
+ # @param answer [Net::HTTPResponse] the latest answer: the response itself, or a 304 that says it hasn't changed
326
+ # @return [void]
327
+ def keep(key, entry, answer)
328
+ keepable = entry.response.instance_of?(Net::HTTPOK) && Freshness.keepable?(answer)
329
+ @lock.synchronize do
330
+ @entries.delete(key)
331
+ hold(key, entry) if keepable
332
+ end
333
+ end
334
+
335
+ # Keep a response as the latest, and no more than the most there may be
336
+ #
337
+ # One too many, and the response kept longest ago is forgotten. The lock is held by whatever calls this.
338
+ #
339
+ # @api private
340
+ # @param key [Array] the URL and the media type asked for, which nothing is kept for
341
+ # @param entry [Entry] the response to keep
342
+ # @return [void]
343
+ def hold(key, entry)
344
+ @entries[key] = entry
345
+ @entries.shift if @entries.size > LIMIT
346
+ end
347
+
348
+ # When a response is good until
349
+ #
350
+ # That's for as long as the latest answer says it's good for, less how long that answer says it has been kept
351
+ # already (Age).
352
+ #
353
+ # @api private
354
+ # @param latest [Net::HTTPResponse] the latest answer: the response itself, or a 304 that says it hasn't changed
355
+ # @return [Numeric] the time on the cache's clock, in seconds
356
+ def expiry(latest)
357
+ @clock.call + Freshness.left(latest)
358
+ end
359
+ end
360
+ private_constant :Cache
361
+ end