JsonApiClient Ruby Client

repository·master·Indexed 18 days ago

https://github.com/jsonapiclient/json_api_client

A Ruby client library for interacting with JSON:API compliant web services. It provides an abstraction layer for resources via JsonApiClient::Resource, featuring a query building framework for CRUD operations, pagination, sparse fieldsets, and compound documents. The library includes support for schema definition with typecasting, custom endpoints, Faraday middleware integration, and detailed server-side validation error handling via ErrorCollector.

Tokens
11.7K
Snippets
47
Records
52
Agent score
63%

What's inside JsonApiClient

  1. Prevent silent failures on blank `.find` parameters

    master

    By default, JsonApiClient::Resource.find(id) returns an array. If id is blank (nil or empty string), the client attempts to call the INDEX endpoint (e.g., /users/) instead of the SHOW endpoint. This can result in silently returning the first available resource from the index. To ensure a JsonApiClient::Errors::NotFound is raised instead, set raise_on_blank_find_param = true in your base resource class.

    class Resource < JsonApiClient::Resource
      self.raise_on_blank_find_param = true
    end
  2. Handle server errors and custom status codes

    master

    Non-success HTTP responses raise specific JsonApiClient::Errors::SomeException subclasses based on the status code. If the response is JSON:API compatible, error messages are parsed into the exception.

    You can override default status handling (e.g., 400 or 401) by providing a handler in connection_options[:status_handlers]. To skip the default exception raising and process errors manually, the handler must call throw(:handled).

    class ApiBadRequestHandler
      def self.call(_env)
        # do not raise exception; call throw(:handled) to skip default
      end
    end
    
    class CustomUnauthorizedError < StandardError
      attr_reader :env
      def initialize(env)
        @env = env
        super('not authorized')
      end
    end
    
    # Override status handling
    MyApi::Base.connection_options[:status_handlers] = {
        400 => ApiBadRequestHandler,
        401 => ->(env) { raise CustomUnauthorizedError, env }
    }
    
    # Usage
    user = MyApi::User.create(name: 'foo')
    # If server responds with 400 and JSON:API errors, user.errors.messages will be populated.
  3. Customize Pagination

    master

    There are three ways to customize pagination:

    1. Change parameter keys globally: Use JsonApiClient::Paginating::Paginator.page_param and per_page_param (Note: this is global).
    2. Use NestedParamPaginator: For JSON:API style page[page]=1&page[per_page]=10, set self.paginator = JsonApiClient::Paginating::NestedParamPaginator on your resource.
    3. Implement a custom Paginator: Create a class that implements current_page, total_entries, etc., and set it via self.paginator = MyPaginator.
    # 1. Global param override
    JsonApiClient::Paginating::Paginator.page_param = "number"
    JsonApiClient::Paginating::Paginator.per_page_param = "size"
    
    # 2. Use NestedParamPaginator for page[page] style
    class Order < JsonApiClient::Resource
      self.paginator = JsonApiClient::Paginating::NestedParamPaginator
    end
    
    # 3. Custom Paginator class
    class MyPaginator
      def initialize(result_set, data); end
      # implement current_page, total_entries, etc.
    end
    
    class MyApi::Base < JsonApiClient::Resource
      self.paginator = MyPaginator
    end
  4. Perform Basic CRUD and Query Operations

    master

    The library provides a query building framework similar to ActiveRecord scopes. All class-level finders and creators return a JsonApiClient::ResultSet, which behaves like an Array but contains extra metadata about the API response.

    Common operations include:

    • .all: Fetch all resources.
    • .where(params): Filter resources.
    • .find(id): Fetch a specific resource by ID.
    • .order(params): Sort results.
    • .includes(associations): Fetch related resources (compound documents).
    • .select(fields): Use sparse fieldsets to fetch only specific attributes.
    • .page(n).per(m) or .paginate(params): Handle pagination.
    # Querying
    MyApi::Article.all
    MyApi::Article.where(author_id: 1).find(2)
    MyApi::Person.where(name: "foo").order(created_at: :desc).includes(:preferences, :cars).all
    
    # Instance lifecycle
    u = MyApi::Person.new(first_name: "bar", last_name: "foo")
    u.new_record? # => true
    u.save
    u.new_record? # => false
    
    # Updating and Destroying
    u = MyApi::Person.find(1)
    u.update_attributes(a: "b", c: "d")
    u.persisted? # => true
    u.destroy
    u.destroyed? # => true
    
    # Creating
    MyApi::Person.create(a: "b", c: "d")
  5. Configure custom connections and Faraday middleware

    master

    You can replace the default connection with a custom class that implements a run method. The default connection uses Faraday, allowing you to inject middleware (like OAuth2 or loggers) via the connection block. It is recommended to configure this in a base model that all resources inherit from.

    # Define a custom connection class
    class NullConnection
      def initialize(*args); end
      def run(request_method, path, params: nil, headers: {}, body: nil); end
      def use(*args); end
    end
    
    # Assign it to a resource
    class CustomConnectionResource < TestResource
      self.connection_class = NullConnection
    end
    
    # Configure middleware in the base class
    MyApi::Base.connection do |connection|
      connection.use FaradayMiddleware::OAuth2, 'MYTOKEN'
      connection.use Faraday::Response::Logger
      connection.use MyCustomMiddleware
    end
  6. Define API Resource Classes

    master

    To use json_api_client, create resource classes that inherit from JsonApiClient::Resource. It is recommended to create an abstract base class to set the API base URL (self.site) and share common behavior across your models. You can namespace your models; namespacing does not affect the URL routing.

    By convention, the library guesses the resource route from the class name (e.g., Article maps to /articles, Person maps to /people).

    module MyApi
      # Abstract base class
      class Base < JsonApiClient::Resource
        self.site = "http://example.com/"
      end
    
      class Article < Base
      end
    
      class Comment < Base
      end
    
      class Person < Base
      end
    end
  7. Handle resource immutability

    master

    A resource can be marked as immutable using the immutable class method. This is useful for resources that should only be read from the API and never modified or deleted by the client.

    When a resource is immutable:

    1. save and destroy will raise JsonApiClient::Errors::ResourceImmutableError.
    2. custom_endpoint definitions are restricted to the :get request method.
    class Configuration < JsonApiClient::Resource
      immutable true
    end
    
    config = Configuration.find(1)
    config.update(setting: 'new') # Raises JsonApiClient::Errors::ResourceImmutableError
  8. Handle API errors with JsonApiClient::Errors

    master

    When interacting with a JSON:API compliant server, JsonApiClient uses a hierarchy of exception classes to represent different failure states. The base class JsonApiClient::Errors::ApiError is designed to automatically extract error details from the response body if they follow the JSON:API errors array format (specifically looking for the title field in each error object).

    Error Hierarchy

    Client Errors (ClientError)

    These represent issues with the request itself (4xx status codes):

    • AccessDenied: Permission issues.
    • NotAuthorized: Authentication issues.
    • NotFound: The requested resource does not exist. Provides access to the uri via the #uri method.
    • Conflict: Resource state conflict (e.g., resource already exists).
    • RequestTimeout: The request timed out.
    • TooManyRequests: Rate limiting encountered.

    Server Errors (ServerError)

    These represent issues on the server side (5xx status codes):

    • InternalServerError: General server failure.
    • BadGateway: Gateway issues.
    • ServiceUnavailable: Server is temporarily down.
    • GatewayTimeout: Gateway timeout.
    • RecordNotSaved: Specific error indicating a record failed to save. Provides access to the record via the #record method.

    Other Errors

    • ConnectionError: Network or connection-level failures.
    • UnexpectedStatus: Raised when an unmapped status code is received. Provides access to #code and #uri.
    • ResourceImmutableError: Raised when attempting to modify an immutable resource.
  9. Cast values using a Schema

    master

    Once a schema is defined, you can use the Property objects within it to cast raw input values into their intended Ruby types. When Property#cast(value) is called:

    1. If the value is nil, it returns nil.
    2. If no type is defined for the property, it returns the value as-is.
    3. If a type is defined, it looks up the corresponding caster in the TypeFactory.
    4. If a caster is found, it calls caster.cast(value, default).
    5. If no caster is found for the specified type, it returns the value as-is.
    schema = JsonApiClient::Schema.new
    schema.add :count, type: :integer, default: 0
    
    # Accessing a property and casting
    property = schema.find(:count)
    
    property.cast("42") # => 42 (Integer)
    property.cast(nil)   # => nil
  10. Track changed attributes using the Dirty helper

    master

    The JsonApiClient::Helpers::Dirty module provides mechanisms to track which attributes on a resource instance have been modified. It allows you to detect changes, retrieve the previous values of changed attributes, and manage the state of these changes. This is useful when you need to perform partial updates or audit changes before sending them to a JSON:API server.

    Key capabilities include:

    • Checking if any attributes have changed using changed?.
    • Retrieving a list of changed attribute names via changed.
    • Accessing the original value of a changed attribute using attribute_was(attr) or dynamic attr_was methods.
    • Manually marking an attribute as changed using attribute_will_change!(attr).
    • Clearing all tracked changes with clear_changes_information.
  11. How the Query Builder constructs parameters

    master

    The params method aggregates all query components into a single hash used for the outgoing request. The order of precedence (later merges overwrite earlier ones) is:

    1. filter_params (from where)
    2. pagination_params (from page/per)
    3. includes_params (from includes)
    4. order_params (from order)
    5. select_params (from select)
    6. primary_key_params (from find or primary_key option)
    7. path_params (from where or initialization)
    8. additional_params (from with_params)

    This ensures that specific identifiers and path parameters take precedence over general filters.