forem-ruby 0.1.0.beta1

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 (74) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/forem-ruby.gemspec +17 -0
  4. data/lib/forem/api_operations/create.rb +47 -0
  5. data/lib/forem/api_operations/delete.rb +88 -0
  6. data/lib/forem/api_operations/list.rb +70 -0
  7. data/lib/forem/api_operations/request.rb +83 -0
  8. data/lib/forem/api_operations/retrieve.rb +43 -0
  9. data/lib/forem/api_operations/save.rb +53 -0
  10. data/lib/forem/api_operations/update.rb +47 -0
  11. data/lib/forem/api_requestor.rb +283 -0
  12. data/lib/forem/api_resource.rb +77 -0
  13. data/lib/forem/client.rb +279 -0
  14. data/lib/forem/configuration.rb +74 -0
  15. data/lib/forem/connection_manager.rb +75 -0
  16. data/lib/forem/errors.rb +118 -0
  17. data/lib/forem/forem_object.rb +264 -0
  18. data/lib/forem/forem_response.rb +50 -0
  19. data/lib/forem/list_object.rb +171 -0
  20. data/lib/forem/resources/admin_concept.rb +169 -0
  21. data/lib/forem/resources/admin_user.rb +152 -0
  22. data/lib/forem/resources/agent_session.rb +110 -0
  23. data/lib/forem/resources/analytics.rb +151 -0
  24. data/lib/forem/resources/article.rb +256 -0
  25. data/lib/forem/resources/billboard.rb +76 -0
  26. data/lib/forem/resources/comment.rb +43 -0
  27. data/lib/forem/resources/concept.rb +192 -0
  28. data/lib/forem/resources/follow.rb +78 -0
  29. data/lib/forem/resources/follower.rb +56 -0
  30. data/lib/forem/resources/health_check.rb +70 -0
  31. data/lib/forem/resources/organization.rb +79 -0
  32. data/lib/forem/resources/page.rb +52 -0
  33. data/lib/forem/resources/podcast_episode.rb +36 -0
  34. data/lib/forem/resources/profile_image.rb +44 -0
  35. data/lib/forem/resources/reaction.rb +61 -0
  36. data/lib/forem/resources/reading_list.rb +29 -0
  37. data/lib/forem/resources/recommended_articles_list.rb +45 -0
  38. data/lib/forem/resources/request_redirect.rb +60 -0
  39. data/lib/forem/resources/segment.rb +103 -0
  40. data/lib/forem/resources/survey.rb +96 -0
  41. data/lib/forem/resources/tag.rb +27 -0
  42. data/lib/forem/resources/trend.rb +80 -0
  43. data/lib/forem/resources/user.rb +229 -0
  44. data/lib/forem/resources/video.rb +28 -0
  45. data/lib/forem/services/admin_concept_service.rb +138 -0
  46. data/lib/forem/services/admin_user_service.rb +114 -0
  47. data/lib/forem/services/agent_session_service.rb +87 -0
  48. data/lib/forem/services/analytics_service.rb +93 -0
  49. data/lib/forem/services/article_service.rb +233 -0
  50. data/lib/forem/services/base_service.rb +45 -0
  51. data/lib/forem/services/billboard_service.rb +91 -0
  52. data/lib/forem/services/comment_service.rb +51 -0
  53. data/lib/forem/services/concept_service.rb +143 -0
  54. data/lib/forem/services/follow_service.rb +61 -0
  55. data/lib/forem/services/follower_service.rb +34 -0
  56. data/lib/forem/services/health_check_service.rb +46 -0
  57. data/lib/forem/services/organization_service.rb +103 -0
  58. data/lib/forem/services/page_service.rb +107 -0
  59. data/lib/forem/services/podcast_episode_service.rb +34 -0
  60. data/lib/forem/services/profile_image_service.rb +32 -0
  61. data/lib/forem/services/reaction_service.rb +72 -0
  62. data/lib/forem/services/reading_list_service.rb +35 -0
  63. data/lib/forem/services/recommended_articles_list_service.rb +87 -0
  64. data/lib/forem/services/request_redirect_service.rb +118 -0
  65. data/lib/forem/services/segment_service.rb +83 -0
  66. data/lib/forem/services/survey_service.rb +48 -0
  67. data/lib/forem/services/tag_service.rb +32 -0
  68. data/lib/forem/services/trend_service.rb +70 -0
  69. data/lib/forem/services/user_service.rb +61 -0
  70. data/lib/forem/services/video_service.rb +33 -0
  71. data/lib/forem/util.rb +43 -0
  72. data/lib/forem/version.rb +4 -0
  73. data/lib/forem.rb +91 -0
  74. metadata +111 -0
@@ -0,0 +1,283 @@
1
+ require "json"
2
+ require "uri"
3
+ require "securerandom"
4
+
5
+ module Forem
6
+ # Executes authenticated HTTP requests against the Forem API.
7
+ #
8
+ # {APIRequestor} is the single entry-point for all network I/O in the
9
+ # library. It builds requests (including authentication headers), delegates
10
+ # transport to a {ConnectionManager}, parses the response, maps HTTP error
11
+ # status codes to typed {ForemError} subclasses, and applies automatic
12
+ # retry logic for transient failures.
13
+ #
14
+ # Each {Forem::Client} owns its own {APIRequestor}, built from the
15
+ # client's {Configuration}. There is no global default requestor — every
16
+ # call must originate from a specific client instance (or pass an
17
+ # explicit +:requestor+ option to a class-level resource method).
18
+ #
19
+ # @example Constructing directly (uncommon — prefer {Forem::Client.new})
20
+ # config = Forem::Configuration.new
21
+ # config.api_key = "my_key"
22
+ # requestor = Forem::APIRequestor.new(config: config)
23
+ # requestor.request(:get, "/api/articles")
24
+ class APIRequestor
25
+ # Create a new APIRequestor.
26
+ #
27
+ # @param config [Configuration] the configuration to use for this
28
+ # requestor.
29
+ # @return [APIRequestor]
30
+ def initialize(config:)
31
+ @config = config
32
+ @connection_manager = ConnectionManager.new
33
+ end
34
+
35
+ # Execute an HTTP request against the Forem API.
36
+ #
37
+ # Builds the request, attaches authentication headers, sends it, and
38
+ # returns a {ForemResponse}. On HTTP 4xx/5xx responses the method raises
39
+ # the appropriate {ForemError} subclass. Transient failures
40
+ # ({APIConnectionError}, {RateLimitError}, server 5xx) are automatically
41
+ # retried up to {Configuration#max_network_retries} times. Rate-limit
42
+ # retries honor integer +Retry-After+ seconds; other retries use
43
+ # exponential back-off.
44
+ #
45
+ # @param method [Symbol] the HTTP verb — +:get+, +:post+, +:put+, or
46
+ # +:delete+.
47
+ # @param path [String] the API path relative to {Configuration#api_base}
48
+ # (e.g. +"/api/articles"+).
49
+ # @param params [Hash] query parameters for GET requests, or the JSON
50
+ # request body for POST/PUT requests. Defaults to +{}+.
51
+ # @param opts [Hash] per-request overrides.
52
+ # @option opts [String] :api_key override the API key for this request.
53
+ # @option opts [String] :api_base override the base URL for this request.
54
+ # @option opts [APIRequestor] :requestor an alternative requestor to use
55
+ # (consumed by higher-level helpers before reaching this method).
56
+ # @return [ForemResponse] the parsed response wrapper.
57
+ # @raise [AuthenticationError] on HTTP 401.
58
+ # @raise [AuthorizationError] on HTTP 403.
59
+ # @raise [NotFoundError] on HTTP 404.
60
+ # @raise [ConflictError] on HTTP 409.
61
+ # @raise [InvalidRequestError] on HTTP 422.
62
+ # @raise [RateLimitError] on HTTP 429.
63
+ # @raise [APIError] on other HTTP 4xx/5xx responses.
64
+ # @raise [APIConnectionError] when a network-level error prevents the
65
+ # request from reaching the server and retries are exhausted.
66
+ #
67
+ # @example Fetching articles with pagination
68
+ # resp = requestor.request(:get, "/api/articles", { page: 2, per_page: 10 })
69
+ # resp.http_status #=> 200
70
+ # resp.parsed_body #=> [{ "id" => 1, ... }, ...]
71
+ #
72
+ # @see https://developers.forem.com/api/v1
73
+ def request(method, path, params = {}, opts = {})
74
+ api_key = opts.delete(:api_key) || @config.api_key
75
+ api_base = opts.delete(:api_base) || @config.api_base
76
+ extra_headers = opts.delete(:headers) || {}
77
+ uri = URI("#{api_base}#{path}")
78
+
79
+ retries_left = @config.max_network_retries
80
+ begin
81
+ response = execute_request(method, uri, params, api_key, extra_headers)
82
+ handle_error_response(response) if response.http_status >= 400
83
+ response
84
+ rescue Forem::APIConnectionError
85
+ if retries_left > 0
86
+ retries_left -= 1
87
+ sleep backoff_duration(@config.max_network_retries - retries_left)
88
+ retry
89
+ end
90
+ raise
91
+ rescue Forem::RateLimitError, Forem::APIError => e
92
+ if retries_left > 0 && retryable_error?(e)
93
+ retries_left -= 1
94
+ retry_count = @config.max_network_retries - retries_left
95
+ sleep retry_delay(e, retry_count)
96
+ retry
97
+ end
98
+ raise
99
+ end
100
+ end
101
+
102
+ private
103
+
104
+ # Send the HTTP request through the connection manager and return a
105
+ # {ForemResponse}.
106
+ #
107
+ # @param method [Symbol] the HTTP verb.
108
+ # @param uri [URI] the fully-qualified request URI.
109
+ # @param params [Hash] parameters/body payload.
110
+ # @param api_key [String, nil] the API key to attach.
111
+ # @return [ForemResponse]
112
+ # @raise [APIConnectionError] on any network-level exception.
113
+ def execute_request(method, uri, params, api_key, extra_headers = {})
114
+ http = @connection_manager.connection_for(uri, open_timeout: @config.open_timeout, read_timeout: @config.read_timeout)
115
+ request = build_request(method, uri, params, api_key, extra_headers)
116
+
117
+ begin
118
+ http_response = http.request(request)
119
+ rescue Net::OpenTimeout, Net::ReadTimeout, Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ETIMEDOUT, Errno::EHOSTUNREACH, SocketError => e
120
+ raise APIConnectionError.new("Connection to #{uri.host} failed: #{e.message}")
121
+ end
122
+
123
+ ForemResponse.new(
124
+ http_status: http_response.code.to_i,
125
+ http_body: http_response.body,
126
+ http_headers: headers_to_hash(http_response)
127
+ )
128
+ end
129
+
130
+ # Build a Net::HTTPRequest object for the given method and parameters.
131
+ #
132
+ # Sets +api-key+, +Accept+, and +User-Agent+ headers on every request.
133
+ # POST and PUT requests encode +params+ as a JSON body and set
134
+ # +Content-Type: application/json+. GET requests append +params+ as a
135
+ # URL query string.
136
+ #
137
+ # @param method [Symbol] the HTTP verb (+:get+, +:post+, +:put+, +:delete+).
138
+ # @param uri [URI] the request URI (modified in-place for GET params).
139
+ # @param params [Hash] the parameters or body payload.
140
+ # @param api_key [String, nil] the API key value.
141
+ # @return [Net::HTTPRequest] the fully-configured request object.
142
+ # @raise [ArgumentError] if +method+ is not one of the supported verbs.
143
+ def build_request(method, uri, params, api_key, extra_headers = {})
144
+ req = case method
145
+ when :get
146
+ uri.query = URI.encode_www_form(params) unless params.empty?
147
+ Net::HTTP::Get.new(uri)
148
+ when :post
149
+ r = Net::HTTP::Post.new(uri)
150
+ r.body = JSON.generate(params) unless params.empty?
151
+ r["Content-Type"] = "application/json"
152
+ r
153
+ when :put
154
+ r = Net::HTTP::Put.new(uri)
155
+ r.body = JSON.generate(params) unless params.empty?
156
+ r["Content-Type"] = "application/json"
157
+ r
158
+ when :delete
159
+ Net::HTTP::Delete.new(uri)
160
+ else
161
+ raise ArgumentError, "Unsupported HTTP method: #{method}"
162
+ end
163
+
164
+ req["api-key"] = api_key if api_key
165
+ req["Accept"] = "application/vnd.forem.api-v1+json"
166
+ req["User-Agent"] = "forem-ruby/#{Forem::VERSION} ruby/#{RUBY_VERSION}"
167
+ extra_headers.each { |name, value| req[name.to_s] = value.to_s }
168
+ req
169
+ end
170
+
171
+ # Raise the appropriate {ForemError} subclass for a non-2xx response.
172
+ #
173
+ # @param response [ForemResponse] the error response.
174
+ # @return [void]
175
+ # @raise [ForemError] always raises a subclass matching the HTTP status.
176
+ def handle_error_response(response)
177
+ message = extract_error_message(response)
178
+ kwargs = {
179
+ http_status: response.http_status,
180
+ http_body: response.http_body,
181
+ http_headers: response.http_headers,
182
+ code: extract_error_code(response),
183
+ }
184
+
185
+ error_class = case response.http_status
186
+ when 401 then AuthenticationError
187
+ when 403 then AuthorizationError
188
+ when 404 then NotFoundError
189
+ when 409 then ConflictError
190
+ when 422 then InvalidRequestError
191
+ when 429 then RateLimitError
192
+ else APIError
193
+ end
194
+
195
+ raise error_class.new(message, **kwargs)
196
+ end
197
+
198
+ # Extract a human-readable error message from an error response.
199
+ #
200
+ # Looks for +error+ (string) or +errors+ (array) keys in the JSON body.
201
+ # Falls back to the raw body string if parsing fails or the expected keys
202
+ # are absent.
203
+ #
204
+ # @param response [ForemResponse] the error response.
205
+ # @return [String] the best available error message.
206
+ def extract_error_message(response)
207
+ body = response.parsed_body
208
+ return response.http_body unless body.is_a?(Hash)
209
+
210
+ if body["error"]
211
+ body["error"]
212
+ elsif body["errors"].is_a?(Array)
213
+ body["errors"].join(", ")
214
+ else
215
+ response.http_body
216
+ end
217
+ rescue JSON::ParserError
218
+ response.http_body
219
+ end
220
+
221
+ # Extract a machine-readable error code from an error response.
222
+ #
223
+ # @param response [ForemResponse] the error response.
224
+ # @return [String, nil] the +error_code+ value, or +nil+ when unavailable.
225
+ def extract_error_code(response)
226
+ body = response.parsed_body
227
+ body["error_code"] if body.is_a?(Hash)
228
+ rescue JSON::ParserError
229
+ nil
230
+ end
231
+
232
+ # Convert a Net::HTTPResponse header enumerable into a plain Hash.
233
+ #
234
+ # @param http_response [Net::HTTPResponse] the raw response object.
235
+ # @return [Hash{String => String}] downcased header names mapped to values.
236
+ def headers_to_hash(http_response)
237
+ h = {}
238
+ http_response.each_header { |k, v| h[k] = v }
239
+ h
240
+ end
241
+
242
+ # Determine whether a given error is eligible for an automatic retry.
243
+ #
244
+ # {RateLimitError} is always retryable. {APIError} is retryable only when
245
+ # the HTTP status is 500 or greater (server errors).
246
+ #
247
+ # @param error [ForemError] the error to evaluate.
248
+ # @return [Boolean] +true+ if the request should be retried.
249
+ def retryable_error?(error)
250
+ case error
251
+ when RateLimitError then true
252
+ when APIError then error.http_status >= 500
253
+ else false
254
+ end
255
+ end
256
+
257
+ # Calculate the delay for a retryable HTTP error.
258
+ #
259
+ # Rate-limit responses with an integer +Retry-After+ header use the
260
+ # server-provided delay. All other retryable errors retain the standard
261
+ # jittered exponential back-off.
262
+ #
263
+ # @param error [ForemError] the retryable error.
264
+ # @param retry_count [Integer] the 1-based retry count.
265
+ # @return [Numeric] seconds to sleep.
266
+ def retry_delay(error, retry_count)
267
+ retry_after = error.retry_after if error.is_a?(RateLimitError)
268
+ retry_after || backoff_duration(retry_count)
269
+ end
270
+
271
+ # Calculate the sleep duration before the next retry attempt.
272
+ #
273
+ # Uses truncated exponential back-off with jitter:
274
+ # <tt>(0.5 * 2^retry_count) + rand(0..0.5)</tt> seconds.
275
+ #
276
+ # @param retry_count [Integer] how many retries have already been attempted
277
+ # (1-based: pass 1 for the first retry).
278
+ # @return [Float] seconds to sleep before retrying.
279
+ def backoff_duration(retry_count)
280
+ (0.5 * (2**retry_count)) + rand * 0.5
281
+ end
282
+ end
283
+ end
@@ -0,0 +1,77 @@
1
+ module Forem
2
+ # Base class for all Forem API resource objects (articles, users, etc.).
3
+ #
4
+ # {APIResource} extends {ForemObject} with the concepts of a canonical
5
+ # resource path and the ability to refresh an instance from the API.
6
+ # Concrete resource classes must define a +RESOURCE_PATH+ constant
7
+ # (e.g. <tt>"/api/articles"</tt>).
8
+ #
9
+ # Resource classes mix in {APIOperations::Request} so that both the class
10
+ # and its instances can issue authenticated HTTP requests. Class methods
11
+ # require an explicit +:requestor+ option (normally injected by a
12
+ # {Forem::Client} via its service objects); instance methods fall back to
13
+ # the requestor stored on the object at construction time.
14
+ #
15
+ # @example Defining a resource subclass
16
+ # class Forem::Article < Forem::APIResource
17
+ # RESOURCE_PATH = "/api/articles"
18
+ # extend APIOperations::List
19
+ # extend APIOperations::Retrieve
20
+ # end
21
+ class APIResource < ForemObject
22
+ # Return the API collection path for this resource class.
23
+ #
24
+ # Delegates to the +RESOURCE_PATH+ constant that every concrete subclass
25
+ # must define.
26
+ #
27
+ # @return [String] the collection path, e.g. <tt>"/api/articles"</tt>.
28
+ # @raise [NameError] if the subclass has not defined +RESOURCE_PATH+.
29
+ #
30
+ # @example
31
+ # Forem::Article.resource_path #=> "/api/articles"
32
+ def self.resource_path
33
+ self::RESOURCE_PATH
34
+ end
35
+
36
+ # Return the API path for this specific resource instance.
37
+ #
38
+ # Combines {.resource_path} with the instance's +id+ attribute.
39
+ #
40
+ # @return [String] the instance path, e.g. <tt>"/api/articles/42"</tt>.
41
+ # @raise [InvalidRequestError] if the instance does not have an +id+
42
+ # attribute (i.e. the object was not constructed from a full API
43
+ # response).
44
+ #
45
+ # @example
46
+ # article.resource_url #=> "/api/articles/42"
47
+ def resource_url
48
+ id = self["id"]
49
+ raise InvalidRequestError.new("Could not determine resource ID") unless id
50
+ "#{self.class.resource_path}/#{id}"
51
+ end
52
+
53
+ # Reload this resource instance from the API, replacing all attributes
54
+ # with the latest server data.
55
+ #
56
+ # @param opts [Hash] per-request options forwarded to {APIRequestor#request}.
57
+ # @option opts [String] :api_key override the API key for this request.
58
+ # @option opts [APIRequestor] :requestor a custom requestor to use.
59
+ # @return [self] the same instance, now populated with refreshed data.
60
+ # @raise [InvalidRequestError] if the instance has no +id+.
61
+ # @raise [NotFoundError] if the resource no longer exists on the server.
62
+ # @raise [ForemError] for other API-level errors.
63
+ #
64
+ # @example
65
+ # article = client.articles.retrieve(42)
66
+ # # ... time passes ...
67
+ # article.refresh #=> same article object with updated attributes
68
+ #
69
+ # @see https://developers.forem.com/api/v1
70
+ def refresh(opts = {})
71
+ resp = request(:get, resource_url, {}, opts)
72
+ @values = {}
73
+ send(:update_attributes, resp.parsed_body)
74
+ self
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,279 @@
1
+ module Forem
2
+ # Client for making authenticated requests to the Forem API.
3
+ #
4
+ # Use this when you need per-instance configuration (e.g., different API keys
5
+ # for different Forem instances). For simple single-instance use, configure
6
+ # the global {Forem.api_key} instead.
7
+ #
8
+ # @example Basic client usage
9
+ # client = Forem::Client.new("your-api-key")
10
+ # articles = client.articles.list(per_page: 10)
11
+ #
12
+ # @example Connecting to a custom Forem instance
13
+ # client = Forem::Client.new("key", api_base: "https://my-forem.com")
14
+ # user = client.users.me
15
+ #
16
+ # @see https://developers.forem.com/api/v1
17
+ class Client
18
+ # @return [Configuration] the configuration object for this client
19
+ attr_reader :config
20
+
21
+ # @return [APIRequestor] the requestor used to make HTTP calls
22
+ attr_reader :requestor
23
+
24
+ # Create a new Forem API client.
25
+ #
26
+ # @param api_key [String] your Forem API key
27
+ # @param api_base [String] base URL for the Forem API (default: "https://dev.to")
28
+ # @param opts [Hash] additional configuration options passed to {Configuration}
29
+ # @return [Client] a new client instance
30
+ #
31
+ # @example
32
+ # client = Forem::Client.new("my-secret-api-key")
33
+ #
34
+ # @example With a custom Forem instance
35
+ # client = Forem::Client.new("my-key", api_base: "https://community.example.com")
36
+ def initialize(api_key, api_base: "https://dev.to", **opts)
37
+ @config = Configuration.new
38
+ @config.api_key = api_key
39
+ @config.api_base = api_base
40
+ opts.each { |k, v| @config.send(:"#{k}=", v) if @config.respond_to?(:"#{k}=") }
41
+ @requestor = APIRequestor.new(config: @config)
42
+ end
43
+
44
+ # Access the Articles API.
45
+ #
46
+ # @return [Services::ArticleService] the articles service
47
+ # @see https://developers.forem.com/api/v1#tag/articles
48
+ #
49
+ # @example
50
+ # client.articles.list(per_page: 5)
51
+ # client.articles.retrieve(12345)
52
+ def articles; @articles ||= Services::ArticleService.new(@requestor); end
53
+
54
+ # Access the Users API.
55
+ #
56
+ # @return [Services::UserService] the users service
57
+ # @see https://developers.forem.com/api/v1#tag/users
58
+ #
59
+ # @example
60
+ # client.users.me
61
+ # client.users.retrieve(42)
62
+ def users; @users ||= Services::UserService.new(@requestor); end
63
+
64
+ # Access the Comments API.
65
+ #
66
+ # @return [Services::CommentService] the comments service
67
+ # @see https://developers.forem.com/api/v1#tag/comments
68
+ #
69
+ # @example
70
+ # client.comments.list(a_id: 123)
71
+ # client.comments.retrieve("abc123")
72
+ def comments; @comments ||= Services::CommentService.new(@requestor); end
73
+
74
+ # Access the Organizations API.
75
+ #
76
+ # @return [Services::OrganizationService] the organizations service
77
+ # @see https://developers.forem.com/api/v1#tag/organizations
78
+ #
79
+ # @example
80
+ # client.organizations.list
81
+ # client.organizations.retrieve(7)
82
+ def organizations; @organizations ||= Services::OrganizationService.new(@requestor); end
83
+
84
+ # Access the Tags API.
85
+ #
86
+ # @return [Services::TagService] the tags service
87
+ # @see https://developers.forem.com/api/v1#tag/tags
88
+ #
89
+ # @example
90
+ # client.tags.list(per_page: 20)
91
+ def tags; @tags ||= Services::TagService.new(@requestor); end
92
+
93
+ # Access the Follows API.
94
+ #
95
+ # @return [Services::FollowService] the follows service
96
+ # @see https://developers.forem.com/api/v1#tag/follows
97
+ #
98
+ # @example
99
+ # client.follows.list
100
+ # client.follows.create(followable_type: "User", followable_id: 99)
101
+ def follows; @follows ||= Services::FollowService.new(@requestor); end
102
+
103
+ # Access the Followers API.
104
+ #
105
+ # @return [Services::FollowerService] the followers service
106
+ # @see https://developers.forem.com/api/v1#tag/followers
107
+ #
108
+ # @example
109
+ # client.followers.list(per_page: 50)
110
+ def followers; @followers ||= Services::FollowerService.new(@requestor); end
111
+
112
+ # Access the Reading List API.
113
+ #
114
+ # @return [Services::ReadingListService] the reading list service
115
+ # @see https://developers.forem.com/api/v1#tag/readinglist
116
+ #
117
+ # @example
118
+ # client.reading_list.list(page: 1)
119
+ def reading_list; @reading_list ||= Services::ReadingListService.new(@requestor); end
120
+
121
+ # Access the Podcast Episodes API.
122
+ #
123
+ # @return [Services::PodcastEpisodeService] the podcast episodes service
124
+ # @see https://developers.forem.com/api/v1#tag/podcast-episodes
125
+ #
126
+ # @example
127
+ # client.podcast_episodes.list(username: "someshow")
128
+ def podcast_episodes; @podcast_episodes ||= Services::PodcastEpisodeService.new(@requestor); end
129
+
130
+ # Access the Videos API.
131
+ #
132
+ # @return [Services::VideoService] the videos service
133
+ # @see https://developers.forem.com/api/v1#tag/videos
134
+ #
135
+ # @example
136
+ # client.videos.list(page: 1)
137
+ def videos; @videos ||= Services::VideoService.new(@requestor); end
138
+
139
+ # Access the Profile Images API.
140
+ #
141
+ # @return [Services::ProfileImageService] the profile images service
142
+ # @see https://developers.forem.com/api/v1#tag/profile-images
143
+ #
144
+ # @example
145
+ # client.profile_images.retrieve("jsmith")
146
+ def profile_images; @profile_images ||= Services::ProfileImageService.new(@requestor); end
147
+
148
+ # Access the Billboards API.
149
+ #
150
+ # @return [Services::BillboardService] the billboards service
151
+ # @see https://developers.forem.com/api/v1#tag/billboards
152
+ #
153
+ # @example
154
+ # client.billboards.list
155
+ # client.billboards.retrieve(3)
156
+ def billboards; @billboards ||= Services::BillboardService.new(@requestor); end
157
+
158
+ # Access the Pages API.
159
+ #
160
+ # @return [Services::PageService] the pages service
161
+ # @see https://developers.forem.com/api/v1#tag/pages
162
+ #
163
+ # @example
164
+ # client.pages.list
165
+ # client.pages.create(title: "About", slug: "about", body_markdown: "...")
166
+ def pages; @pages ||= Services::PageService.new(@requestor); end
167
+
168
+ # Access the Segments API.
169
+ #
170
+ # @return [Services::SegmentService] the segments service
171
+ # @see https://developers.forem.com/api/v1#tag/segments
172
+ #
173
+ # @example
174
+ # client.segments.list
175
+ # client.segments.retrieve(5)
176
+ def segments; @segments ||= Services::SegmentService.new(@requestor); end
177
+
178
+ # Access the Reactions API.
179
+ #
180
+ # @return [Services::ReactionService] the reactions service
181
+ # @see https://developers.forem.com/api/v1#tag/reactions
182
+ #
183
+ # @example
184
+ # client.reactions.create(reactable_type: "Article", reactable_id: 1, category: "like")
185
+ # client.reactions.toggle(reactable_type: "Article", reactable_id: 1, category: "like")
186
+ def reactions; @reactions ||= Services::ReactionService.new(@requestor); end
187
+
188
+ # Access the Recommended Articles Lists API.
189
+ #
190
+ # @return [Services::RecommendedArticlesListService] the recommended articles lists service
191
+ # @see https://developers.forem.com/api/v1#tag/articles
192
+ #
193
+ # @example
194
+ # client.recommended_articles_lists.list
195
+ # client.recommended_articles_lists.retrieve(2)
196
+ def recommended_articles_lists; @recommended_articles_lists ||= Services::RecommendedArticlesListService.new(@requestor); end
197
+
198
+ # Access the Agent Sessions API.
199
+ #
200
+ # @return [Services::AgentSessionService] the agent sessions service
201
+ # @see https://developers.forem.com/api/v1
202
+ #
203
+ # @example
204
+ # client.agent_sessions.list
205
+ # client.agent_sessions.presign(filename: "upload.jpg")
206
+ def agent_sessions; @agent_sessions ||= Services::AgentSessionService.new(@requestor); end
207
+
208
+ # Access the Surveys API.
209
+ #
210
+ # @return [Services::SurveyService] the surveys service
211
+ # @see https://developers.forem.com/api/v1
212
+ #
213
+ # @example
214
+ # client.surveys.list
215
+ # client.surveys.retrieve(8)
216
+ def surveys; @surveys ||= Services::SurveyService.new(@requestor); end
217
+
218
+ # Access the Analytics API.
219
+ #
220
+ # @return [Services::AnalyticsService] the analytics service
221
+ # @see https://developers.forem.com/api/v1#tag/analytics
222
+ #
223
+ # @example
224
+ # client.analytics.totals(username: "jsmith")
225
+ # client.analytics.historical(username: "jsmith", start: "2024-01-01")
226
+ def analytics; @analytics ||= Services::AnalyticsService.new(@requestor); end
227
+
228
+ # Access the Health Checks API.
229
+ #
230
+ # @return [Services::HealthCheckService] the health check service
231
+ # @see https://developers.forem.com/api/v1#tag/health-checks
232
+ #
233
+ # @example
234
+ # client.health_checks.app
235
+ # client.health_checks.database
236
+ def health_checks; @health_checks ||= Services::HealthCheckService.new(@requestor); end
237
+
238
+ # Access the Admin Users API.
239
+ #
240
+ # @return [Services::AdminUserService] the admin users service
241
+ # @see https://developers.forem.com/api/v1#tag/users
242
+ #
243
+ # @example
244
+ # client.admin_users.create(email: "new@example.com", name: "New User")
245
+ def admin_users; @admin_users ||= Services::AdminUserService.new(@requestor); end
246
+
247
+ # Access the Trends API.
248
+ #
249
+ # @return [Services::TrendService] the trends service
250
+ #
251
+ # @example
252
+ # client.trends.list
253
+ def trends; @trends ||= Services::TrendService.new(@requestor); end
254
+
255
+ # Access the Concepts API.
256
+ #
257
+ # @return [Services::ConceptService] the concepts service
258
+ #
259
+ # @example
260
+ # client.concepts.list
261
+ def concepts; @concepts ||= Services::ConceptService.new(@requestor); end
262
+
263
+ # Access the Admin Concepts API.
264
+ #
265
+ # @return [Services::AdminConceptService] the admin concepts service
266
+ #
267
+ # @example
268
+ # client.admin_concepts.list
269
+ def admin_concepts; @admin_concepts ||= Services::AdminConceptService.new(@requestor); end
270
+
271
+ # Access the Admin Request Redirects API.
272
+ #
273
+ # @return [Services::RequestRedirectService] the request redirects service
274
+ #
275
+ # @example
276
+ # client.request_redirects.list
277
+ def request_redirects; @request_redirects ||= Services::RequestRedirectService.new(@requestor); end
278
+ end
279
+ end