From 6885caebedeb580ba60f662cdd4c96a48ec49666 Mon Sep 17 00:00:00 2001 From: Kuba Suder Date: Mon, 12 Jan 2026 23:26:37 +0200 Subject: [PATCH] Add YARD documentation --- lib/minisky/errors.rb | 22 ++++++++ lib/minisky/minisky.rb | 15 ++++++ lib/minisky/requests.rb | 104 +++++++++++++++++++++++++++++++++++++ lib/minisky/version.rb | 2 + spec/custom_client_spec.rb | 7 +++ 5 files changed, 150 insertions(+) diff --git a/lib/minisky/errors.rb b/lib/minisky/errors.rb index 7bdffcb..3e81da6 100644 --- a/lib/minisky/errors.rb +++ b/lib/minisky/errors.rb @@ -1,18 +1,27 @@ require_relative 'minisky' class Minisky + # Base error class for Minisky. class Error < StandardError end + # Raised when authentication or credentials are invalid. class AuthError < Error + # @param message [String] def initialize(message) super(message) end end + # Raised when the API returns a non-success response. class BadResponse < Error + # @return [Integer] HTTP status code + # @return [Object] parsed response data attr_reader :status, :data + # @param status [Integer] + # @param status_message [String] + # @param data [Object] def initialize(status, status_message, data) @status = status @data = data @@ -26,36 +35,49 @@ def initialize(status, status_message, data) super(message) end + # @return [String, nil] error type from response data def error_type @data['error'] if @data.is_a?(Hash) end + # @return [String, nil] error message from response data def error_message @data['message'] if @data.is_a?(Hash) end end + # Client error response (4xx). class ClientErrorResponse < BadResponse end + # Server error response (5xx). class ServerErrorResponse < BadResponse end + # Expired access token response. class ExpiredTokenError < ClientErrorResponse end + # Raised when a redirect is encountered unexpectedly. class UnexpectedRedirect < BadResponse + # @return [String] redirect location attr_reader :location + # @param status [Integer] + # @param status_message [String] + # @param location [String] def initialize(status, status_message, location) super(status, status_message, { 'message' => "Unexpected redirect: #{location}" }) @location = location end end + # Raised when fetch_all cannot determine the response field. class FieldNotSetError < Error + # @return [Array] attr_reader :fields + # @param fields [Array] def initialize(fields) @fields = fields super("Field parameter not provided; available fields: #{@fields.inspect}") diff --git a/lib/minisky/minisky.rb b/lib/minisky/minisky.rb index f269742..3d4a272 100644 --- a/lib/minisky/minisky.rb +++ b/lib/minisky/minisky.rb @@ -1,8 +1,17 @@ require 'yaml' +# Main client for interacting with AT Protocol servers. class Minisky + # @return [String] the host name or base URL for the server + # @return [Hash] the loaded configuration data attr_reader :host, :config + # Create a new client instance. + # + # @param host [String] the host name or base URL for the server + # @param config_file [String, nil] path to the YAML config file + # @param options [Hash] optional attribute overrides to apply + # @raise [AuthError] if the config file is missing required credentials def initialize(host, config_file, options = {}) @host = host @config_file = config_file @@ -30,12 +39,18 @@ def initialize(host, config_file, options = {}) end end + # Check whether the current process looks like an interactive REPL. + # + # @return [Boolean] true when running inside IRB or Pry def active_repl? return true if defined?(IRB) && IRB.respond_to?(:CurrentContext) && IRB.CurrentContext return true if defined?(Pry) && Pry.respond_to?(:cli) && Pry.cli false end + # Persist the current configuration to disk. + # + # @return [void] def save_config File.write(@config_file, YAML.dump(@config)) if @config_file end diff --git a/lib/minisky/requests.rb b/lib/minisky/requests.rb index b20afba..d1046e0 100644 --- a/lib/minisky/requests.rb +++ b/lib/minisky/requests.rb @@ -8,15 +8,23 @@ require 'uri' class Minisky + # Lightweight wrapper around a mutable configuration hash. class User + # @param config [Hash] backing configuration hash def initialize(config) @config = config end + # @return [Boolean] whether the user has both access and refresh tokens def logged_in? !!(access_token && refresh_token) end + # Forward unknown getters/setters to the config hash. + # + # @param name [Symbol] + # @param args [Array] + # @return [Object] def method_missing(name, *args) if name.to_s.end_with?('=') @config[name.to_s.chop] = args[0] @@ -26,17 +34,24 @@ def method_missing(name, *args) end end + # Regular expression that matches AT Protocol NSID identifiers. NSID_REGEXP = /^[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(\.[a-zA-Z]([a-zA-Z]{0,61}[a-zA-Z])?)$/ + # HTTP request helpers for the Minisky client. module Requests + # @return [String, nil] character printed for progress indication attr_accessor :default_progress + # @return [Boolean] whether to send auth headers automatically attr_writer :send_auth_headers + # @return [Boolean] whether to automatically manage access tokens attr_writer :auto_manage_tokens + # @return [Boolean] whether auth headers are enabled def send_auth_headers instance_variable_defined?('@send_auth_headers') ? @send_auth_headers : true end + # @return [Boolean] whether token management is enabled def auto_manage_tokens instance_variable_defined?('@auto_manage_tokens') ? @auto_manage_tokens : true end @@ -44,6 +59,9 @@ def auto_manage_tokens alias progress default_progress alias progress= default_progress= + # Build the base XRPC URL for this client. + # + # @return [String] def base_url if host.include?('://') host.chomp('/') + '/xrpc' @@ -52,10 +70,19 @@ def base_url end end + # @return [Minisky::User] accessor for configuration-backed user data def user @user ||= User.new(config) end + # Perform a GET request. + # + # @param method [String, URI] NSID name or URI + # @param params [Hash, nil] query parameters + # @param auth [Boolean, String] whether to use auth headers or bearer token string + # @param headers [Hash, nil] extra headers to include + # @return [Hash, String] parsed JSON or raw response body + # @raise [AuthError, BadResponse] def get_request(method, params = nil, auth: default_auth_mode, headers: nil) check_access if auto_manage_tokens && auth == true @@ -72,6 +99,15 @@ def get_request(method, params = nil, auth: default_auth_mode, headers: nil) handle_response(response) end + # Perform a POST request. + # + # @param method [String, URI] NSID name or URI + # @param data [Hash, String, nil] JSON payload + # @param auth [Boolean, String] whether to use auth headers or bearer token string + # @param headers [Hash, nil] extra headers to include + # @param params [Hash, nil] query parameters + # @return [Hash, String] parsed JSON or raw response body + # @raise [AuthError, BadResponse] def post_request(method, data = nil, auth: default_auth_mode, headers: nil, params: nil) check_access if auto_manage_tokens && auth == true @@ -94,6 +130,18 @@ def post_request(method, data = nil, auth: default_auth_mode, headers: nil, para handle_response(response) end + # Fetch paginated records until the cursor ends or a break condition is met. + # + # @param method [String, URI] NSID name or URI + # @param params [Hash, nil] query parameters + # @param auth [Boolean, String] whether to use auth headers or bearer token string + # @param field [String, Symbol, nil] response field containing the records array + # @param break_when [Proc, nil] optional predicate to stop when any record matches + # @param max_pages [Integer, nil] maximum number of pages to request + # @param headers [Hash, nil] extra headers to include + # @param progress [String, nil] progress indicator printed each request + # @return [Array] collected records + # @raise [FieldNotSetError] def fetch_all(method, params = nil, auth: default_auth_mode, field: nil, break_when: nil, max_pages: nil, headers: nil, progress: @default_progress) data = [] @@ -124,6 +172,9 @@ def fetch_all(method, params = nil, auth: default_auth_mode, data end + # Ensure access tokens are valid when auto-management is enabled. + # + # @return [Symbol] :logged_in, :refreshed, or :ok def check_access if !user.logged_in? log_in @@ -136,6 +187,10 @@ def check_access end end + # Authenticate with the server and store tokens. + # + # @return [Hash] response JSON + # @raise [AuthError] def log_in if user.id.nil? || user.pass.nil? raise AuthError, "To log in, please provide a user id and password" @@ -156,6 +211,10 @@ def log_in json end + # Refresh the access token using the stored refresh token. + # + # @return [Hash] response JSON + # @raise [AuthError] def perform_token_refresh if user.refresh_token.nil? raise AuthError, "Can't refresh access token - refresh token is missing" @@ -170,6 +229,11 @@ def perform_token_refresh json end + # Parse the access token expiry from a JWT. + # + # @param token [String] JWT access token + # @return [Time] expiration time + # @raise [AuthError] def token_expiration_date(token) parts = token.split('.') raise AuthError, "Invalid access token format" unless parts.length == 3 @@ -186,10 +250,14 @@ def token_expiration_date(token) Time.at(exp) end + # @return [Boolean] whether the access token expires within 60 seconds def access_token_expired? token_expiration_date(user.access_token) < Time.now + 60 end + # Clear stored access and refresh tokens. + # + # @return [nil] def reset_tokens user.access_token = nil user.refresh_token = nil @@ -202,11 +270,27 @@ def reset_tokens alias_method :do_post_request, :post_request private :do_get_request, :do_post_request + # Backwards-compatible keyword support for Ruby 2. + # + # @param method [String, URI] NSID name or URI + # @param params [Hash, nil] query parameters + # @param auth [Boolean, String] whether to use auth headers or bearer token string + # @param headers [Hash, nil] extra headers to include + # @param kwargs [Hash] keyword params for older callers + # @return [Hash, String] parsed JSON or raw response body def get_request(method, params = nil, auth: default_auth_mode, headers: nil, **kwargs) params ||= kwargs unless kwargs.empty? do_get_request(method, params, auth: auth, headers: headers) end + # Backwards-compatible keyword support for Ruby 2. + # + # @param method [String, URI] NSID name or URI + # @param params [Hash, nil] JSON payload + # @param auth [Boolean, String] whether to use auth headers or bearer token string + # @param headers [Hash, nil] extra headers to include + # @param kwargs [Hash] keyword params for older callers + # @return [Hash, String] parsed JSON or raw response body def post_request(method, params = nil, auth: default_auth_mode, headers: nil, **kwargs) params ||= kwargs unless kwargs.empty? do_post_request(method, params, auth: auth, headers: headers) @@ -216,6 +300,10 @@ def post_request(method, params = nil, auth: default_auth_mode, headers: nil, ** private + # Execute the HTTP request and return the raw response. + # + # @param request [Net::HTTPRequest] + # @return [Net::HTTPResponse] def make_request(request) # this long form is needed because #get_response only supports a headers param in Ruby 3.x response = Net::HTTP.start(request.uri.hostname, request.uri.port, use_ssl: true) do |http| @@ -223,6 +311,11 @@ def make_request(request) end end + # Build a request URI from an NSID, URL, or URI object. + # + # @param method [String, URI] + # @return [URI] + # @raise [ArgumentError] def build_request_uri(method) if method.is_a?(URI) method @@ -235,10 +328,16 @@ def build_request_uri(method) end end + # @return [Boolean] default auth mode def default_auth_mode !!send_auth_headers end + # Build the authorization header for a request. + # + # @param auth [Boolean, String] + # @return [Hash] + # @raise [AuthError] def authentication_header(auth) if auth.is_a?(String) { 'Authorization' => "Bearer #{auth}" } @@ -253,6 +352,11 @@ def authentication_header(auth) end end + # Raise errors or return parsed response bodies. + # + # @param response [Net::HTTPResponse] + # @return [Hash, String] + # @raise [BadResponse] def handle_response(response) status = response.code.to_i message = response.message diff --git a/lib/minisky/version.rb b/lib/minisky/version.rb index 4be311d..ced9d33 100644 --- a/lib/minisky/version.rb +++ b/lib/minisky/version.rb @@ -1,5 +1,7 @@ require_relative 'minisky' class Minisky + # Current gem version. + # @return [String] VERSION = "0.5.0" end diff --git a/spec/custom_client_spec.rb b/spec/custom_client_spec.rb index 59fe730..bf11af9 100644 --- a/spec/custom_client_spec.rb +++ b/spec/custom_client_spec.rb @@ -1,21 +1,28 @@ require 'json' require_relative 'shared/ex_unauthed' +# Custom JSON-backed client for request specs. class CustomJSONClient + # @return [String] path to the test JSON config CONFIG_FILE = 'test.json' + # Request helpers for the test client. include Minisky::Requests + # @return [Hash] parsed configuration data attr_reader :config + # @return [void] def initialize @config = JSON.parse(File.read(CONFIG_FILE)) end + # @return [String] test host name def host 'at.x.com' end + # @return [void] def save_config File.write(CONFIG_FILE, JSON.generate(@config)) end