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,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "resource"
4
+ require_relative "collection"
5
+ require_relative "talk"
6
+ require_relative "place"
7
+ require_relative "podcast"
8
+
9
+ module Sferik
10
+ # Conference talks and podcasts, as returned by {API::TalkEndpoints#talks}
11
+ #
12
+ # It's Enumerable over its {#talks}, so `Sferik.talks.first` is the newest, and `Sferik.talks.map(&:title)` lists
13
+ # them all. It also has what an Array adds to that: {#size}, {#length}, {#empty?}, {#last}, and {#[]}.
14
+ #
15
+ # @api public
16
+ class Talks < Resource
17
+ include Enumerable
18
+ include Collection
19
+
20
+ # ActiveSupport's Enumerable#as_json would make a list of them; keep the JSON it came from, as for any resource
21
+ define_method(:as_json, Resource.instance_method(:as_json))
22
+
23
+ # @!method talks
24
+ # The talks, newest first
25
+ # @api public
26
+ # @return [Array<Talk>] the talks, newest first
27
+ # @example
28
+ # talks.talks # => [#<Sferik::Talk ...>, ...]
29
+ list :talks, "talks", type: Talk
30
+
31
+ # @!method places
32
+ # Where each talk's location is, by the name {Talk#location} gives it
33
+ # @api public
34
+ # @return [Hash{String => Place}] where each talk's location is, by name
35
+ # @example
36
+ # talks.places["Barcelona"] # => #<Sferik::Place lat=41.39 lon=2.17 country="Spain">
37
+ dictionary :places, type: Place
38
+
39
+ # @!method podcasts
40
+ # The podcast appearances
41
+ # @api public
42
+ # @return [Array<Podcast>] the podcast appearances
43
+ # @example
44
+ # talks.podcasts # => [#<Sferik::Podcast ...>, ...]
45
+ list :podcasts, "podcasts", type: Podcast
46
+
47
+ # @!method speaker_deck
48
+ # Where the slides are
49
+ # @api public
50
+ # @return [String] where the slides are
51
+ # @example
52
+ # talks.speaker_deck # => "https://speakerdeck.com/sferik"
53
+ attribute :speaker_deck
54
+
55
+ # @!method command
56
+ # The shell command the home page shows the newest talks as
57
+ # @api public
58
+ # @return [String] the shell command the home page shows the newest talks as
59
+ # @example
60
+ # talks.command # => "ls -t ~/talks | head -6"
61
+ attribute :command
62
+
63
+ inspect_with :size, :speaker_deck
64
+
65
+ # Yield each talk, newest first
66
+ #
67
+ # @api public
68
+ # @yieldparam talk [Talk] a talk
69
+ # @return [self, Enumerator<Talk>] the talks themselves, or an Enumerator without a block
70
+ # @example
71
+ # Sferik.talks.each { |talk| puts talk.title }
72
+ def each(&block)
73
+ return to_enum { size } unless block
74
+
75
+ talks.each(&block)
76
+ self
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Sferik
6
+ # Checks the options of a client and the arguments of its requests, for {Client}, and the attributes of a {Resource}
7
+ #
8
+ # A wrong one raises ArgumentError where it's given, rather than an error in the middle of a request.
9
+ #
10
+ # @api private
11
+ module Validation
12
+ private
13
+
14
+ # Check the type of an option, an argument, or a resource's attributes
15
+ #
16
+ # A wrong one fails here rather than in the middle of a request.
17
+ #
18
+ # @api private
19
+ # @param option [Symbol] the option's name, or the argument's: :path, :body, :accept, :token, :page, :tty, :attributes, or :key
20
+ # @param value [Object] its value
21
+ # @param type [Class] the class its value should be of
22
+ # @return [Object] the value
23
+ # @raise [ArgumentError] if the value isn't of that class
24
+ def check(option, value, type)
25
+ return value if value.is_a?(type)
26
+
27
+ raise ArgumentError, "#{option} must be #{type}, not #{value.inspect}"
28
+ end
29
+
30
+ # Check that a host is an http or https URL, and no more than one
31
+ #
32
+ # One without a scheme, like "localhost:3745", fails here rather than in the middle of a request. So does one
33
+ # with a query or a fragment, which a path added to it would be part of, and one with credentials, which are
34
+ # never sent.
35
+ #
36
+ # @api private
37
+ # @param value [String] the host
38
+ # @return [String] the host
39
+ # @raise [ArgumentError] if the host isn't an http or https URL, or has credentials, a query, or a fragment
40
+ def http_url(value)
41
+ uri = URI.parse(value)
42
+ raise URI::InvalidURIError unless uri.is_a?(URI::HTTP) && !uri.host.to_s.empty?
43
+ return value if [uri.userinfo, uri.query, uri.fragment].none?
44
+
45
+ raise ArgumentError, "host must have no credentials, query, or fragment, not #{value.inspect}"
46
+ rescue URI::InvalidURIError
47
+ raise ArgumentError, "host must be an http or https URL, not #{value.inspect}"
48
+ end
49
+
50
+ # Check that an option is true or false
51
+ #
52
+ # @api private
53
+ # @param option [Symbol] the option's name
54
+ # @param value [Object] its value
55
+ # @return [Boolean] the value
56
+ # @raise [ArgumentError] if the value is neither true nor false
57
+ def boolean(option, value)
58
+ return value if [true, false].include?(value)
59
+
60
+ raise ArgumentError, "#{option} must be true or false, not #{value.inspect}"
61
+ end
62
+
63
+ # Check that the value of a header is on one line
64
+ #
65
+ # Net::HTTP fails in the middle of a request with one that has a carriage return or a line feed.
66
+ #
67
+ # @api private
68
+ # @param option [Symbol] the option's name, or :accept
69
+ # @param value [String] its value
70
+ # @return [String] the value
71
+ # @raise [ArgumentError] if the value has a line break
72
+ def one_line(option, value)
73
+ return value unless value.match?(/[\r\n]/)
74
+
75
+ raise ArgumentError, "#{option} must be on one line, not #{value.inspect}"
76
+ end
77
+
78
+ # Check that a timeout is a number of seconds, positive and finite
79
+ #
80
+ # @api private
81
+ # @param option [Symbol] the option's name
82
+ # @param value [Object] its value
83
+ # @return [Numeric] the value
84
+ # @raise [ArgumentError] if the value isn't Numeric, or isn't positive and finite
85
+ def seconds(option, value)
86
+ positive(option, check(option, value, Numeric))
87
+ end
88
+
89
+ # Check that a timeout is positive and finite
90
+ #
91
+ # Net::HTTP waits forever with one of 0, or an infinite one, and fails in the middle of a request with a negative
92
+ # one. A complex number is Numeric, but is neither positive nor negative.
93
+ #
94
+ # @api private
95
+ # @param option [Symbol] the option's name
96
+ # @param value [Numeric] its value
97
+ # @return [Numeric] the value
98
+ # @raise [ArgumentError] if the value isn't a real number, isn't finite, or isn't positive
99
+ def positive(option, value)
100
+ return value if value.real? && value.finite? && value.positive?
101
+
102
+ raise ArgumentError, "#{option} must be positive and finite, not #{value}"
103
+ end
104
+
105
+ # Check that a limit isn't negative
106
+ #
107
+ # @api private
108
+ # @param option [Symbol] the option's name
109
+ # @param value [Integer] its value
110
+ # @return [Integer] the value
111
+ # @raise [ArgumentError] if the value is negative
112
+ def not_negative(option, value)
113
+ return value unless value.negative?
114
+
115
+ raise ArgumentError, "#{option} must be 0 or more, not #{value}"
116
+ end
117
+ end
118
+ private_constant :Validation
119
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sferik
4
+ # The version of the library
5
+ # @api public
6
+ VERSION = "0.0.1"
7
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../resource"
4
+
5
+ module Sferik
6
+ class WebFinger < Resource
7
+ # Something an account links to, as returned in {WebFinger#links}
8
+ #
9
+ # A link has a URL or a template for one, not both.
10
+ #
11
+ # @api public
12
+ class Link < Resource
13
+ # @!method rel
14
+ # What the link is to the account
15
+ # @api public
16
+ # @return [String] what the link is to the account
17
+ # @example
18
+ # link.rel # => "http://webfinger.net/rel/profile-page"
19
+ attribute :rel
20
+
21
+ # @!method type
22
+ # The media type of what's at the URL
23
+ # @api public
24
+ # @return [String, nil] the media type of what's at the URL, or nil for a link with a template
25
+ # @example
26
+ # link.type # => "text/html"
27
+ attribute :type
28
+
29
+ # @!method href
30
+ # The URL
31
+ # @api public
32
+ # @return [String, nil] the URL, or nil for a link with a template
33
+ # @example
34
+ # link.href # => "https://mastodon.social/@sferik"
35
+ attribute :href
36
+
37
+ # @!method template
38
+ # A template for a URL, with the account to follow from as {uri}
39
+ # @api public
40
+ # @return [String, nil] the template, or nil for a link with a URL
41
+ # @example
42
+ # link.template # => "https://mastodon.social/authorize_interaction?uri={uri}"
43
+ attribute :template
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "resource"
4
+ require_relative "web_finger/link"
5
+
6
+ module Sferik
7
+ # Where an account at sferik.net points to, as returned by {API::ProfileEndpoints#webfinger}
8
+ #
9
+ # It's a JSON Resource Descriptor, as WebFinger (RFC 7033) answers with: the account on Mastodon, which makes the
10
+ # domain a fediverse handle.
11
+ #
12
+ # @api public
13
+ class WebFinger < Resource
14
+ # @!method subject
15
+ # The account this one points to, as an acct: URI
16
+ # @api public
17
+ # @return [String] the account this one points to, as an acct: URI
18
+ # @example
19
+ # webfinger.subject # => "acct:sferik@mastodon.social"
20
+ attribute :subject
21
+
22
+ # @!method aliases
23
+ # The URLs the account goes by
24
+ # @api public
25
+ # @return [Array<String>] the URLs the account goes by
26
+ # @example
27
+ # webfinger.aliases # => ["https://mastodon.social/@sferik", "https://mastodon.social/users/sferik"]
28
+ list :aliases
29
+
30
+ # @!method links
31
+ # What the account links to
32
+ #
33
+ # That's its profile page, itself as ActivityPub, and where to follow it.
34
+ #
35
+ # @api public
36
+ # @return [Array<Link>] what the account links to
37
+ # @example
38
+ # webfinger.links # => [#<Sferik::WebFinger::Link ...>, ...]
39
+ list :links, "links", type: Link
40
+
41
+ inspect_with :subject
42
+ end
43
+ end
data/lib/sferik/who.rb ADDED
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "resource"
4
+ require_relative "collection"
5
+ require_relative "session"
6
+
7
+ module Sferik
8
+ # Everyone reading the site right now, as returned by {API::SiteEndpoints#who} and {API::SiteEndpoints#check_in}
9
+ #
10
+ # It's Enumerable over its {#users}, so `Sferik.who.map(&:page)` lists the pages being read. It also has what an
11
+ # Array adds to that: {#size}, {#length}, {#empty?}, {#last}, and {#[]}.
12
+ #
13
+ # @api public
14
+ class Who < Resource
15
+ include Enumerable
16
+ include Collection
17
+
18
+ # ActiveSupport's Enumerable#as_json would make a list of them; keep the JSON it came from, as for any resource
19
+ define_method(:as_json, Resource.instance_method(:as_json))
20
+
21
+ # @!method users
22
+ # The terminals logged in: one per browser tab
23
+ # @api public
24
+ # @return [Array<Session>] the terminals logged in: one per browser tab
25
+ # @example
26
+ # who.users # => [#<Sferik::Session tty="ttys001" page="/" login=2026-10-06 12:00:00 UTC>]
27
+ list :users, "users", type: Session
28
+
29
+ # @!method you
30
+ # The terminal that checked in, from {API::SiteEndpoints#check_in}
31
+ # @api public
32
+ # @return [String, nil] the terminal's name: nil when every terminal is taken, or when nobody checked in
33
+ # @example
34
+ # Sferik.check_in(token).you # => "ttys001"
35
+ attribute :you
36
+
37
+ inspect_with :size
38
+
39
+ # Yield each terminal
40
+ #
41
+ # @api public
42
+ # @yieldparam session [Session] a terminal
43
+ # @return [self, Enumerator<Session>] the terminals themselves, or an Enumerator without a block
44
+ # @example
45
+ # Sferik.who.each { |session| puts session.tty }
46
+ def each(&block)
47
+ return to_enum { size } unless block
48
+
49
+ users.each(&block)
50
+ self
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "resource"
4
+ require_relative "block"
5
+
6
+ module Sferik
7
+ # The bio, as returned by {API::ProfileEndpoints#whoami}
8
+ # @api public
9
+ class Whoami < Resource
10
+ # @!method command
11
+ # The shell command the home page shows it as
12
+ # @api public
13
+ # @return [String] the shell command the home page shows it as
14
+ # @example
15
+ # whoami.command # => "whoami"
16
+ attribute :command
17
+
18
+ # @!method blocks
19
+ # The paragraphs, in order
20
+ # @api public
21
+ # @return [Array<Block>] the paragraphs, in order
22
+ # @example
23
+ # whoami.blocks # => [#<Sferik::Block ...>, ...]
24
+ list :blocks, "blocks", type: Block
25
+
26
+ # @!method multi_downloads
27
+ # The combined RubyGems downloads of multi_json and multi_xml
28
+ # @api public
29
+ # @return [Integer] the combined RubyGems downloads of multi_json and multi_xml
30
+ # @example
31
+ # whoami.multi_downloads # => 1_776_825_140
32
+ attribute :multi_downloads
33
+
34
+ inspect_with :command, :multi_downloads
35
+ end
36
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "errors"
4
+ require_relative "validation"
5
+
6
+ module Sferik
7
+ # Checks the attributes of a resource, freezes the values of a response, and wraps them in resources, for {Resource}
8
+ # @api private
9
+ module Wrapping
10
+ include Validation
11
+
12
+ private
13
+
14
+ # A value of the response, or something else when the response has nothing there
15
+ #
16
+ # Nothing is null, which is what a key that's missing reads as too. False isn't nothing: it's a value, and where
17
+ # a list or an object belongs, the wrong one.
18
+ #
19
+ # @api private
20
+ # @param value [Object] the value
21
+ # @param otherwise [Object] what to return when the response has nothing there
22
+ # @return [Object] the value, or what was given in its place
23
+ def given(value, otherwise)
24
+ value.nil? ? otherwise : value
25
+ end
26
+
27
+ # Wrap a value in a resource class
28
+ #
29
+ # @api private
30
+ # @param value [Object] the value
31
+ # @param type [Class, nil] the resource class, or nil to leave the value as it is
32
+ # @return [Object] the value, wrapped
33
+ # @raise [InvalidResponse] if the value isn't a JSON object
34
+ def wrap(value, type)
35
+ (type.nil? || value.nil?) ? value : build(type, value)
36
+ end
37
+
38
+ # Wrap each value of a list in a resource class
39
+ #
40
+ # @api private
41
+ # @param name [Symbol] the reader of the list
42
+ # @param list [Object] the list
43
+ # @param type [Class, nil] the resource class, or nil to leave the values as they are
44
+ # @return [Array<Object>] the values, wrapped and frozen
45
+ # @raise [InvalidResponse] if the list isn't a JSON array, or one in it isn't a JSON object
46
+ def wrap_list(name, list, type)
47
+ raise InvalidResponse, "#{self.class}##{name}: expected a JSON array, got #{list.class}" unless list.instance_of?(Array)
48
+
49
+ type ? list.map { |item| build(type, item) }.freeze : list
50
+ end
51
+
52
+ # Wrap each value of a dictionary in a resource class
53
+ #
54
+ # @api private
55
+ # @param name [Symbol] the reader of the dictionary
56
+ # @param dictionary [Object] the dictionary
57
+ # @param type [Class] the resource class
58
+ # @return [Hash{String => Resource}] the values, wrapped and frozen
59
+ # @raise [InvalidResponse] if the dictionary isn't a JSON object, or one in it isn't
60
+ def wrap_dictionary(name, dictionary, type)
61
+ raise InvalidResponse, "#{self.class}##{name}: expected a JSON object, got #{dictionary.class}" unless dictionary.instance_of?(Hash)
62
+
63
+ dictionary.transform_values { |item| build(type, item) }.freeze
64
+ end
65
+
66
+ # Parse an ISO 8601 date or time
67
+ #
68
+ # @api private
69
+ # @param name [Symbol] the reader of the date or time
70
+ # @param value [Object] the value
71
+ # @param parser [#iso8601] Time, Date, or PartialDate for a date that may be given to the year or month
72
+ # @return [Time, Date, nil] the date or time, frozen, or nil if the response has none
73
+ # @raise [InvalidResponse] if the value isn't an ISO 8601 date or time
74
+ def wrap_timestamp(name, value, parser)
75
+ parser.iso8601(value).freeze unless value.nil?
76
+ rescue ArgumentError, TypeError
77
+ raise InvalidResponse, "#{self.class}##{name}: #{value.inspect} isn't an ISO 8601 #{parser.equal?(Time) ? "time" : "date"}"
78
+ end
79
+
80
+ # Build a resource from a JSON object
81
+ #
82
+ # @api private
83
+ # @param type [Class] the resource class
84
+ # @param value [Object] the value
85
+ # @return [Resource] the resource
86
+ # @raise [InvalidResponse] if the value isn't a JSON object
87
+ def build(type, value)
88
+ raise InvalidResponse, "Expected a JSON object for a #{type}, got #{value.class}" unless value.instance_of?(Hash)
89
+
90
+ type.new(value)
91
+ end
92
+
93
+ # A frozen copy of a value and everything in it
94
+ #
95
+ # The keys of every Hash must be strings, as the keys of JSON are: a reader's name as a Symbol (start_date:) is
96
+ # not the key the reader reads ("startDate"), so it fails here rather than reading as nil. What it's given is
97
+ # left as it was: a String that isn't frozen is copied, not frozen in place. The other values of JSON (numbers,
98
+ # true, false, and nil) are frozen as they are.
99
+ #
100
+ # @api private
101
+ # @param value [Object] the value
102
+ # @return [Object] the copy, frozen
103
+ # @raise [ArgumentError] if a key of a Hash isn't a String
104
+ def deep_freeze(value)
105
+ case value
106
+ when Hash then value.to_h { |key, item| [check(:key, key, String), deep_freeze(item)] }.freeze # a Hash freezes its String keys itself
107
+ when Array then value.map { |item| deep_freeze(item) }.freeze
108
+ when String then -value
109
+ else value
110
+ end
111
+ end
112
+ end
113
+ private_constant :Wrapping
114
+ end