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.
- checksums.yaml +7 -0
- data/.yardopts +5 -0
- data/CHANGELOG.md +72 -0
- data/LICENSE.md +16 -0
- data/README.md +517 -0
- data/exe/sferik +6 -0
- data/lib/sferik/api/code_endpoints.rb +37 -0
- data/lib/sferik/api/profile_endpoints.rb +113 -0
- data/lib/sferik/api/resume_endpoints.rb +41 -0
- data/lib/sferik/api/site_endpoints.rb +247 -0
- data/lib/sferik/api/talk_endpoints.rb +45 -0
- data/lib/sferik/api.rb +23 -0
- data/lib/sferik/block.rb +25 -0
- data/lib/sferik/body.rb +54 -0
- data/lib/sferik/cache.rb +361 -0
- data/lib/sferik/cli.rb +369 -0
- data/lib/sferik/client.rb +453 -0
- data/lib/sferik/collection.rb +82 -0
- data/lib/sferik/configuration.rb +129 -0
- data/lib/sferik/connections.rb +151 -0
- data/lib/sferik/contributions.rb +86 -0
- data/lib/sferik/day.rb +35 -0
- data/lib/sferik/dependency.rb +26 -0
- data/lib/sferik/deployment.rb +36 -0
- data/lib/sferik/errors.rb +234 -0
- data/lib/sferik/figure.rb +83 -0
- data/lib/sferik/finger.rb +74 -0
- data/lib/sferik/freshness.rb +54 -0
- data/lib/sferik/home/pages.rb +27 -0
- data/lib/sferik/home/profile.rb +51 -0
- data/lib/sferik/home/section.rb +27 -0
- data/lib/sferik/home.rb +38 -0
- data/lib/sferik/json_parsing.rb +28 -0
- data/lib/sferik/name_change.rb +49 -0
- data/lib/sferik/place.rb +33 -0
- data/lib/sferik/podcast.rb +41 -0
- data/lib/sferik/project.rb +49 -0
- data/lib/sferik/projects.rb +138 -0
- data/lib/sferik/push.rb +33 -0
- data/lib/sferik/resource.rb +373 -0
- data/lib/sferik/resume/award.rb +43 -0
- data/lib/sferik/resume/basics.rb +77 -0
- data/lib/sferik/resume/education.rb +59 -0
- data/lib/sferik/resume/location.rb +35 -0
- data/lib/sferik/resume/meta.rb +35 -0
- data/lib/sferik/resume/patent.rb +43 -0
- data/lib/sferik/resume/profile.rb +35 -0
- data/lib/sferik/resume/project.rb +27 -0
- data/lib/sferik/resume/skill.rb +27 -0
- data/lib/sferik/resume/speaking.rb +19 -0
- data/lib/sferik/resume/volunteer.rb +59 -0
- data/lib/sferik/resume/work.rb +51 -0
- data/lib/sferik/resume.rb +125 -0
- data/lib/sferik/session.rb +41 -0
- data/lib/sferik/social_profile.rb +49 -0
- data/lib/sferik/status/github.rb +38 -0
- data/lib/sferik/status/loaded.rb +48 -0
- data/lib/sferik/status.rb +33 -0
- data/lib/sferik/talk.rb +73 -0
- data/lib/sferik/talks.rb +79 -0
- data/lib/sferik/validation.rb +119 -0
- data/lib/sferik/version.rb +7 -0
- data/lib/sferik/web_finger/link.rb +46 -0
- data/lib/sferik/web_finger.rb +43 -0
- data/lib/sferik/who.rb +53 -0
- data/lib/sferik/whoami.rb +36 -0
- data/lib/sferik/wrapping.rb +114 -0
- data/lib/sferik.rb +246 -0
- data/sig/manifest.yaml +13 -0
- data/sig/sferik.rbs +825 -0
- metadata +118 -0
data/lib/sferik/talks.rb
ADDED
|
@@ -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,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
|