{"id":13879177,"url":"https://github.com/local-ch/lhs","last_synced_at":"2025-05-15T15:07:25.751Z","repository":{"id":20812813,"uuid":"24098393","full_name":"local-ch/lhs","owner":"local-ch","description":"⚛️ REST services accelerator: Rails gem providing an easy, active-record-like interface for http (hypermedia) json services","archived":false,"fork":false,"pushed_at":"2024-12-18T22:25:09.000Z","size":1737,"stargazers_count":137,"open_issues_count":49,"forks_count":3,"subscribers_count":43,"default_branch":"master","last_synced_at":"2024-12-21T09:33:29.765Z","etag":null,"topics":["http-client","hypermedia-api","hypermedia-client","json-api","query-chain","request-cycle-cache","ruby"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/local-ch.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2014-09-16T12:25:31.000Z","updated_at":"2024-07-12T05:53:51.000Z","dependencies_parsed_at":"2025-01-02T15:03:18.774Z","dependency_job_id":null,"html_url":"https://github.com/local-ch/lhs","commit_stats":{"total_commits":752,"total_committers":28,"mean_commits":"26.857142857142858","dds":0.5678191489361701,"last_synced_commit":"7c7c912b872682ae3e1c5c0bc68d0ca0b4272326"},"previous_names":[],"tags_count":295,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/local-ch%2Flhs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/local-ch%2Flhs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/local-ch%2Flhs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/local-ch%2Flhs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/local-ch","download_url":"https://codeload.github.com/local-ch/lhs/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247730068,"owners_count":20986404,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["http-client","hypermedia-api","hypermedia-client","json-api","query-chain","request-cycle-cache","ruby"],"created_at":"2024-08-06T08:02:12.327Z","updated_at":"2025-04-07T21:10:07.406Z","avatar_url":"https://github.com/local-ch.png","language":"Ruby","readme":"LHS\n===\n\nLHS ia a Rails-Gem, providing an ActiveRecord like interface to access HTTP-JSON-Services from Rails Applications. Special features provided by this gem are: Multiple endpoint configuration per resource, active-record-like query-chains, scopes, error handling, relations, request cycle cache, batch processing, including linked resources (hypermedia), data maps (data accessing), nested-resource handling, ActiveModel like backend validation conversion, formbuilder-compatible, three types of pagination support, service configuration per resource, kaminari-support and much more.\n\nLHS uses [LHC](//github.com/local-ch/LHC) for advanced http requests.\n\n## Quickstart\n\n```\ngem 'lhs'\n```\n\n```ruby\n# config/initializers/lhc.rb\n\nLHC.configure do |config|\n  config.placeholder(:service, 'https://my.service.dev')\nend\n```\n\n```ruby\n# app/models/record.rb\n\nclass Record \u003c LHS::Record\n\n  endpoint '{+service}/records'\n  endpoint '{+service}/records/{id}'\n\nend\n```\n\n```ruby\n# app/controllers/application_controller.rb\n\nrecord = Record.find_by(email: 'somebody@mail.com')\nrecord.review # \"Lunch was great\n```\n\n## Table of contents\n   * [LHS](#lhs)\n  * [Quickstart](#quickstart)\n  * [Installation/Startup checklist](#installationstartup-checklist)\n  * [Record](#record)\n     * [Endpoints](#endpoints)\n        * [Configure endpoint hosts](#configure-endpoint-hosts)\n        * [Endpoint Priorities](#endpoint-priorities)\n     * [Provider](#provider)\n     * [Record inheritance](#record-inheritance)\n     * [Find multiple records](#find-multiple-records)\n        * [fetch](#fetch)\n        * [where](#where)\n        * [Reuse/Dry where statements: Use scopes](#reusedry-where-statements-use-scopes)\n        * [all](#all)\n        * [all with unpaginated endpoints](#all-with-unpaginated-endpoints)\n        * [Retrieve the amount of a collection of items: count vs. length](#retrieve-the-amount-of-a-collection-of-items-count-vs-length)\n     * [Find single records](#find-single-records)\n        * [find](#find)\n        * [find_by](#find_by)\n        * [first](#first)\n        * [last](#last)\n     * [Work with retrieved data](#work-with-retrieved-data)\n        * [Automatic detection/conversion of collections](#automatic-detectionconversion-of-collections)\n        * [Map complex data for easy access](#map-complex-data-for-easy-access)\n        * [Access and identify nested records](#access-and-identify-nested-records)\n           * [Relations / Associations](#relations--associations)\n              * [has_many](#has_many)\n              * [has_one](#has_one)\n        * [Unwrap nested items from the response body](#unwrap-nested-items-from-the-response-body)\n        * [Determine collections from the response body](#determine-collections-from-the-response-body)\n        * [Load additional data based on retrieved data](#load-additional-data-based-on-retrieved-data)\n     * [Chain complex queries](#chain-complex-queries)\n        * [Chain where queries](#chain-where-queries)\n        * [Expand plain collections of links: expanded](#expand-plain-collections-of-links-expanded)\n        * [Error handling with chains](#error-handling-with-chains)\n        * [Resolve chains: fetch](#resolve-chains-fetch)\n        * [Add request options to a query chain: options](#add-request-options-to-a-query-chain-options)\n        * [Control pagination within a query chain](#control-pagination-within-a-query-chain)\n     * [Record pagination](#record-pagination)\n        * [Pagination strategy](#pagination-strategy)\n           * [Pagination strategy: offset (default)](#pagination-strategy-offset-default)\n           * [Pagination strategy: page](#pagination-strategy-page)\n           * [Pagination strategy: start](#pagination-strategy-start)\n           * [Pagination strategy: link](#pagination-strategy-link)\n        * [Pagination keys](#pagination-keys)\n           * [limit_key](#limit_key)\n           * [pagination_key](#pagination_key)\n           * [total_key](#total_key)\n        * [Pagination links](#pagination-links)\n           * [next?](#next)\n           * [previous?](#previous)\n        * [Kaminari support (limited)](#kaminari-support-limited)\n     * [Build, create and update records](#build-create-and-update-records)\n        * [Create new records](#create-new-records)\n           * [create](#create)\n              * [Unwrap nested data when creation response nests created record data](#unwrap-nested-data-when-creation-response-nests-created-record-data)\n              * [Create records through associations: Nested sub resources](#create-records-through-associations-nested-sub-resources)\n        * [Start building new records](#start-building-new-records)\n        * [Change/Update existing records](#changeupdate-existing-records)\n           * [save](#save)\n           * [update](#update)\n              * [Directly via Record](#directly-via-record)\n              * [per Instance](#per-instance)\n           * [partial_update](#partial_update)\n        * [Endpoint url parameter injection during record creation/change](#endpoint-url-parameter-injection-during-record-creationchange)\n        * [Record validation](#record-validation)\n           * [Configure record validations](#configure-record-validations)\n           * [HTTP Status Codes for validation errors](#http-status-codes-for-validation-errors)\n           * [Reset validation errors](#reset-validation-errors)\n           * [Add validation errors](#add-validation-errors)\n           * [Validation errors for nested data](#validation-errors-for-nested-data)\n           * [Translation of validation errors](#translation-of-validation-errors)\n           * [Validation error types: errors vs. warnings](#validation-error-types-errors-vs-warnings)\n              * [Persistance failed: errors](#persistance-failed-errors)\n              * [Persistance succeeded: warnings](#persistance-succeeded-warnings)\n           * [Using ActiveModel::Validations none the less](#using-activemodelvalidations-none-the-less)\n        * [Use form_helper to create and update records](#use-form_helper-to-create-and-update-records)\n     * [Destroy records](#destroy-records)\n     * [Record getters and setters](#record-getters-and-setters)\n        * [Record setters](#record-setters)\n        * [Record getters](#record-getters)\n     * [Include linked resources (hyperlinks and hypermedia)](#include-linked-resources-hyperlinks-and-hypermedia)\n        * [Generate links from parameters](#generate-links-from-parameters)\n        * [Ensure the whole linked collection is included with includes](#ensure-the-whole-linked-collection-is-included-with-includes)\n        * [Include only the first linked page of a linked collection: includes_first_page](#include-only-the-first-linked-page-of-a-linked-collection-includes_first_page)\n        * [Include various levels of linked data](#include-various-levels-of-linked-data)\n        * [Identify and cast known records when including records](#identify-and-cast-known-records-when-including-records)\n        * [Apply options for requests performed to fetch included records](#apply-options-for-requests-performed-to-fetch-included-records)\n        * [compact: Remove included resources that didn't return any records](#compact-remove-included-resources-that-didnt-return-any-records)\n     * [Record batch processing](#record-batch-processing)\n        * [all](#all-1)\n           * [Using all, when endpoint does not implement response pagination meta data](#using-all-when-endpoint-does-not-implement-response-pagination-meta-data)\n        * [find_each](#find_each)\n        * [find_in_batches](#find_in_batches)\n     * [Convert/Cast specific record types: becomes](#convertcast-specific-record-types-becomes)\n     * [Assign attributes](#assign-attributes)\n  * [Request Cycle Cache](#request-cycle-cache)\n     * [Change store for LHS' request cycle cache](#change-store-for-lhs-request-cycle-cache)\n     * [Disable request cycle cache](#disable-request-cycle-cache)\n  * [Automatic Authentication (OAuth)](#automatic-authentication-oauth)\n     * [Configure multiple auth providers (even per endpoint)](#configure-multiple-auth-providers-even-per-endpoint)\n     * [Configure providers](#configure-providers)\n  * [Option Blocks](#option-blocks)\n  * [Request tracing](#request-tracing)\n  * [Extended Rollbar Logging](#extended-rollbar-logging)\n  * [Testing with LHS](#testing-with-lhs)\n     * [Test helper](#test-helper)\n        * [Stub](#stub)\n           * [stub_all](#stub_all)\n     * [Test query chains](#test-query-chains)\n        * [By explicitly resolving the chain: fetch](#by-explicitly-resolving-the-chain-fetch)\n        * [Without resolving the chain: where_values_hash](#without-resolving-the-chain-where_values_hash)\n  * [Extended developer documentation](#extended-developer-documentation)\n     * [Accessing data in LHS](#accessing-data-in-lhs)\n  * [License](#license)\n\n\n\n\n\n\n## Installation/Startup checklist\n\n- [ ] Install LHS gem, preferably via `Gemfile`\n- [ ] Configure [LHC](https://github.com/local-ch/lhc) via an `config/initializers/lhc.rb` (See: https://github.com/local-ch/lhc#configuration)\n- [ ] Add `LHC::Caching` to `LHC.config.interceptors` to facilitate LHS' [Request Cycle Cache](#request-cycle-cache)\n- [ ] Store all LHS::Records in `app/models` for autoload/preload reasons\n- [ ] Request data from services via `LHS` from within your rails controllers\n\n## Record\n\n### Endpoints\n\n\u003e Endpoint, the entry point to a service, a process, a queue or a topic in a service-oriented architecture\n\nStart a record with configuring one or multiple endpoints.\n\n```ruby\n# app/models/record.rb\n\nclass Record \u003c LHS::Record\n\n  endpoint '{+service}/records'\n  endpoint '{+service}/records/{id}'\n  endpoint '{+service}/accociation/{accociation_id}/records'\n  endpoint '{+service}/accociation/{accociation_id}/records/{id}'\n\nend\n```\n\nYou can also add request options to be used with configured endpoints:\n\n```ruby\n# app/models/record.rb\n\nclass Record \u003c LHS::Record\n\n  endpoint '{+service}/records', auth: { bearer: -\u003e { access_token } }\n  endpoint '{+service}/records/{id}', auth: { bearer: -\u003e { access_token } }\n\nend\n```\n\n-\u003e Check [LHC](https://github.com/local-ch/lhc) for more information about request options\n\n#### Configure endpoint hosts\n\nIt's common practice to use different hosts accross different environments in a service-oriented architecture.\n\nUse [LHC placeholders](https://github.com/local-ch/lhc#configuring-placeholders) to configure different hosts per environment:\n\n```ruby\n# config/initializers/lhc.rb\n\nLHC.configure do |config|\n  config.placeholder(:search, ENV['SEARCH'])\nend\n```\n\n```ruby\n# app/models/record.rb\n\nclass Record \u003c LHS::Record\n\n  endpoint '{+search}/api/search.json'\n\nend\n```\n\n**DON'T!**\n\nPlease DO NOT mix host placeholders with and endpoint's resource path, as otherwise LHS will not work properly.\n\n```ruby\n# config/initializers/lhc.rb\n\nLHC.configure do |config|\n  config.placeholder(:search, 'http://tel.search.ch/api/search.json')\nend\n```\n\n```ruby\n# app/models/record.rb\n\nclass Record \u003c LHS::Record\n\n  endpoint '{+search}'\n  \nend\n```\n\n#### Endpoint Priorities\n\nLHS uses endpoint configurations to determine what endpoint to use when data is requested, in a similar way, routes are identified in Rails to map requests to controllers.\n\nIf they are ambiguous, LHS will always use the first one found:\n\n```ruby\n# app/models/record.rb\n\nclass Record \u003c LHS::Record\n\n  endpoint '{+service}/records'\n  endpoint '{+service}/bananas'\n\nend\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nRecord.fetch\n```\n```\nGET https://service.example.com/records\n```\n\n**Be aware that, if you configure ambiguous endpoints across multiple classes, the order of things is not deterministic. Ambiguous endpoints across multiple classes need to be avoided.**\n\n### Provider\n\nProviders in LHS allow you to group shared endpoint options under a common provider.\n\n```ruby\n# app/models/provider/base_record.rb\n\nmodule Provider\n  class BaseRecord \u003c LHS::Record\n    provider params: { api_key: 123 }\n  end\nend\n```\n\nNow every record, part of that particular provider can inherit the provider's `BaseRecord`.\n\n```ruby\n# app/models/provider/account.rb\n\nmodule Provider\n  class Account \u003c BaseRecord\n    endpoint '{+host}/records'\n    endpoint '{+host}/records/{id}'\n  end\nend\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nProvider::Account.find(1)\n```\n```\nGET https://provider/records/1?api_key=123\n```\n\nAnd requests made via those provider records apply the common provider options.\n\n### Record inheritance\n\nYou can inherit from previously defined records and also inherit endpoints that way:\n\n```ruby\n# app/models/base.rb\n\nclass Base \u003c LHS::Record\n  endpoint '{+service}/records/{id}'\nend\n```\n\n```ruby\n# app/models/record.rb\n\nclass Record \u003c Base\nend\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nRecord.find(1)\n```\n```\nGET https://service.example.com/records/1\n```\n\n### Find multiple records\n\n#### fetch\n\nIn case you want to just fetch the records endpoint, without applying any further queries or want to handle pagination, you can simply call `fetch`:\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecords = Record.fetch\n\n```\n```\n  GET https://service.example.com/records\n```\n\n#### where\n\nYou can query a service for records by using `where`:\n\n```ruby\n# app/controllers/some_controller.rb\n\nRecord.where(color: 'blue')\n\n```\n```\n  GET https://service.example.com/records?color=blue\n```\n\nIf the provided parameter – `color: 'blue'` in this case – is not part of the endpoint path, it will be added as query parameter.\n\n```ruby\n# app/controllers/some_controller.rb\n\nRecord.where(accociation_id: '12345')\n\n```\n```\nGET https://service.example.com/accociation/12345/records\n```\n\nIf the provided parameter – `accociation_id` in this case – is part of the endpoint path, it will be injected into the path.\n\nYou can also provide hrefs to fetch multiple records:\n\n```ruby\n# app/controllers/some_controller.rb\n\nRecord.where('https://service.example.com/accociation/12345/records')\n\n```\n```\nGET https://service.example.com/accociation/12345/records\n```\n\n\n#### Reuse/Dry where statements: Use scopes\n\nIn order to reuse/dry where statements organize them in scopes:\n\n```ruby\n# app/models/record.rb\n\nclass Record \u003c LHS::Record\n\n  endpoint '{+service}/records'\n  endpoint '{+service}/records/{id}'\n\n  scope :blue, -\u003e { where(color: 'blue') }\n  scope :available, -\u003e(state) { where(available: state) }\n\nend\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecords = Record.blue.available(true)\n```\n```\nGET https://service.example.com/records?color=blue\u0026available=true\n```\n\n#### all\n\nYou can fetch all remote records by using `all`. Pagination will be performed automatically (See: [Record pagination](#record-pagination))\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecords = Record.all\n\n```\n```\n  GET https://service.example.com/records?limit=100\n  GET https://service.example.com/records?limit=100\u0026offset=100\n  GET https://service.example.com/records?limit=100\u0026offset=200\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecords.size # 300\n\n```\n\n#### all with unpaginated endpoints\n\nIn case your record endpoints are not implementing any pagination, configure it to be `paginated: false`. Pagination will not be performed automatically in those cases:\n\n```ruby\n# app/models/record.rb\n\nclass Record \u003c LHS::Record\n  configuration paginated: false\nend\n\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecords = Record.all\n\n```\n```\n  GET https://service.example.com/records\n```\n\n#### Retrieve the amount of a collection of items: count vs. length\n\nThe different behavior of `count` and `length` is based on ActiveRecord's behavior.\n\n`count` The total number of items available remotly via the provided endpoint/api, communicated via pagination meta data.\n\n`length` The number of items already loaded from the endpoint/api and kept in memory right now. In case of a paginated endpoint this can differ to what `count` returns, as it depends on how many pages have been loaded already.\n\n### Find single records\n\n#### find\n\n`find` finds a unique record by unique identifier (usually `id` or `href`). If no record is found an error is raised.\n\n```ruby\nRecord.find(123)\n```\n```\nGET https://service.example.com/records/123\n```\n\n```ruby\nRecord.find('https://anotherservice.example.com/records/123')\n```\n```\nGET https://anotherservice.example.com/records/123\n```\n\n`find` can also be used to find a single unique record with parameters:\n\n```ruby\nRecord.find(another_identifier: 456)\n```\n```\nGET https://service.example.com/records?another_identifier=456\n```\n\nYou can also fetch multiple records by `id` in parallel:\n\n```ruby\nRecord.find(1, 2, 3)\n```\n```\n# In parallel:\n  GET https://service.example.com/records/1\n  GET https://service.example.com/records/2\n  GET https://service.example.com/records/3\n```\n\n#### find_by\n\n`find_by` finds the first record matching the specified conditions. If no record is found, `nil` is returned.\n\n`find_by!` raises `LHC::NotFound` if nothing was found.\n\n```ruby\nRecord.find_by(color: 'blue')\n```\n```\nGET https://service.example.com/records?color=blue\n```\n\n#### first\n\n`first` is an alias for finding the first record without parameters. If no record is found, `nil` is returned.\n\n`first!` raises `LHC::NotFound` if nothing was found.\n\n```ruby\nRecord.first\n```\n```\nGET https://service.example.com/records?limit=1\n```\n\n`first` can also be used with options:\n\n```ruby\nRecord.first(params: { color: :blue })\n```\n```\nGET https://service.example.com/records?color=blue\u0026limit=1\n```\n\n#### last\n\n`last` is an alias for finding the last record without parameters. If no record is found, `nil` is returned.\n\n`last!` raises `LHC::NotFound` if nothing was found.\n\n```ruby\nRecord.last\n```\n\n`last` can also be used with options:\n\n```ruby\nRecord.last(params: { color: :blue })\n```\n\n### Work with retrieved data\n\nAfter fetching [single](#find-single-records) or [multiple](#find-multiple-records) records you can navigate the received data with ease:\n\n```ruby\nrecords = Record.where(color: 'blue')\nrecords.length # 4\nrecords.count # 400\nrecord = records.first\nrecord.type # 'Business'\nrecord[:type] # 'Business'\nrecord['type'] # 'Business'\n```\n\n#### Automatic detection/conversion of collections\n\nHow to configure endpoints for automatic collection detection?\n\nLHS detects automatically if the responded data is a single business object or a set of business objects (collection).\n\nConventionally, when the respons contains an `items` key `{ items: [] }` it's treated as a collection, but also if the respons contains a plain raw array: `[{ href: '' }]` it's also treated as a collection.\n\nIf you need to configure the attribute of the response providing the collection, configure `items_key` as explained here: [Determine collections from the response body](#determine-collections-from-the-response-body)\n\n#### Map complex data for easy access\n\nTo influence how data is accessed, simply create methods inside your Record to access complex data structures:\n\n```ruby\n# app/models/record.rb\n\nclass Record \u003c LHS::Record\n\n  endpoint '{+service}/records'\n\n  def name\n    dig(:addresses, :first, :business, :identities, :first, :name)\n  end\nend\n```\n\n#### Access and identify nested records\n\nNested records, in nested data, are automatically casted to the correct Record class, when they provide an `href` and that `href` matches any defined endpoint of any defined Record:\n\n```ruby\n# app/models/place.rb\n\nclass Place \u003c LHS::Record\n  endpoint '{+service}/places'\n  endpoint '{+service}/places/{id}'\n\n  def name\n    dig(:addresses, :first, :business, :identities, :first, :name)\n  end\nend\n```\n\n```ruby\n# app/models/favorite.rb\n\nclass Favorite \u003c LHS::Record\n  endpoint '{+service}/favorites'\n  endpoint '{+service}/favorites/{id}'\nend\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nfavorite = Favorite.includes(:place).find(123)\nfavorite.place.name # local.ch AG\n```\n```\nGET https://service.example.com/favorites/123\n\n{... place: { href: 'https://service.example.com/places/456' }}\n\nGET https://service.example.com/places/456\n```\n\nIf automatic detection of nested records does not work, make sure your Records are stored in `app/models`! See: [Insallation/Startup checklist](#installationstartup-checklist)\n\n##### Relations / Associations\n\nTypically nested data is automatically casted when accessed (See: [Access and identify nested records](#access-and-identify-nested-records)), but sometimes API's don't provide dedicated endpoints to retrieve these records.\nIn those cases, those records are only available through other records and don't have an `href` on their own and can't be casted automatically, when accessed. \n\nTo be able to implement Record-specific logic for those nested records, you can define relations/associations.\n\n###### has_many\n\n```ruby\n# app/models/location.rb\n\nclass Location \u003c LHS::Record\n\n  endpoint '{+service}/locations/{id}'\n\n  has_many :listings\n\nend\n```\n\n```ruby\n# app/models/listing.rb\n\nclass Listing \u003c LHS::Record\n\n  def supported?\n    type == 'SUPPORTED'\n  end\nend\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nLocation.find(1).listings.first.supported? # true\n```\n```\nGET https://service.example.com/locations/1\n{... listings: [{ type: 'SUPPORTED' }] }\n```\n\n`class_name`: Specify the class name of the relation. Use it only if that name can't be inferred from the relation name. So has_many :photos will by default be linked to the Photo class, but if the real class name is e.g. CustomPhoto or namespaced Custom::Photo, you'll have to specify it with this option.\n\n```ruby\n# app/models/custom/location.rb\n\nmodule Custom\n  class Location \u003c LHS::Record\n    endpoint '{+service}/locations'\n    endpoint '{+service}/locations/{id}'\n    \n    has_many :photos, class_name: 'Custom::Photo'\n  end\nend\n```\n\n```ruby\n# app/models/custom/photo.rb\n\nmodule Custom\n  class Photo \u003c LHS::Record\n  end\nend\n```\n\n###### has_one\n\n```ruby\n# app/models/transaction.rb\n\nclass Transaction \u003c LHS::Record\n\n  endpoint '{+service}/transaction/{id}'\n\n  has_one :user\nend\n```\n\n```ruby\n# app/models/user.rb\n\nclass User \u003c LHS::Record\n\n  def email\n    self[:email_address]\n  end\nend\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nTransaction.find(1).user.email_address # steve@local.ch\n```\n```\nGET https://service.example.com/transaction/1\n{... user: { email_address: 'steve@local.ch' } }\n```\n\n`class_name`: Specify the class name of the relation. Use it only if that name can't be inferred from the relation name. So has_many :photos will by default be linked to the Photo class, but if the real class name is e.g. CustomPhoto or namespaced Custom::Photo, you'll have to specify it with this option.\n\n```ruby\n# app/models/custom/location.rb\n\nmodule Custom\n  class Location \u003c LHS::Record\n    endpoint '{+service}/locations'\n    endpoint '{+service}/locations/{id}'\n    \n    has_one :photo, class_name: 'Custom::Photo'\n  end\nend\n```\n\n```ruby\n# app/models/custom/photo.rb\n\nmodule Custom\n  class Photo \u003c LHS::Record\n  end\nend\n```\n\n#### Unwrap nested items from the response body\n\nIf the actual item data is mixed with metadata in the response body, LHS allows you to configure a record in a way to automatically unwrap items from within nested response data.\n\n`item_key` is used to unwrap the actual object from within the response body.\n\n```ruby\n# app/models/location.rb\n\nclass Location \u003c LHS::Record\n  configuration item_key: [:response, :location]\nend\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nlocation = Location.find(123)\nlocation.id # 123\n```\n```\nGET https://service.example.com/locations/123\n{... response: { location: { id: 123 } } }\n```\n\n#### Determine collections from the response body\n\n`items_key` key used to determine the collection of items of the current page (e.g. `docs`, `items`, etc.), defaults to 'items':\n\n```ruby\n# app/models/search.rb\n\nclass Search \u003c LHS::Record\n  configuration items_key: :docs\nend\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nsearch_result = Search.where(q: 'Starbucks')\nsearch_result.first.address # Bahnhofstrasse 5, 8000 Zürich\n```\n```\nGET https://service.example.com/search?q=Starbucks\n{... docs: [... {...  address: 'Bahnhofstrasse 5, 8000 Zürich' }] }\n```\n\n#### Load additional data based on retrieved data\n\nIn order to load linked data from already retrieved data, you can use `load!` (or `reload!`).\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecord = Record.find(1)\nrecord.associated_thing.load!\n```\n```\nGET https://things/4\n{ name: \"Steve\" }\n```\n```ruby\n# app/controllers/some_controller.rb\nrecord.associated_thing.name # Steve\n\nrecord.associated_thing.load! # Does NOT create another request, as it is already loaded\nrecord.associated_thing.reload! # Does request the data again from remote\n\n```\n```\nGET https://things/4\n{ name: \"Steve\" }\n```\n\n### Chain complex queries\n\n\u003e [Method chaining](https://en.wikipedia.org/wiki/Method_chaining), also known as named parameter idiom, is a common syntax for invoking multiple method calls in object-oriented programming languages. Each method returns an object, allowing the calls to be chained together without requiring variables to store the intermediate results\n\nIn order to simplify and enhance preparing complex queries for performing single or multiple requests, LHS implements query chains to find single or multiple records. \n\nLHS query chains do [lazy evaluation](https://de.wikipedia.org/wiki/Lazy_Evaluation) to only perform as many requests as needed, when the data to be retrieved is actually needed.\n\nAny method, accessing the content of the data to be retrieved, is resolving the chain in place – like `.each`, `.first`, `.some_attribute_name`. Nevertheless, if you just want to resolve the chain in place, and nothing else, `fetch` should be the method of your choice:\n\n```ruby\n# app/controllers/some_controller.rb\n\nRecord.where(color: 'blue').fetch\n```\n\n#### Chain where queries\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecords = Record.where(color: 'blue')\n[...]\nrecords.where(available: true).each do |record|\n  [...]\nend\n```\n```\n  GET https://service.example.com/records?color=blue\u0026available=true\n```\n\nIn case you wan't to check/debug the current values for where in the chain, you can use `where_values_hash`:\n\n```ruby\nrecords.where_values_hash\n\n# {color: 'blue', available: true}\n```\n\n#### Expand plain collections of links: expanded\n\nSome endpoints could respond only with a plain list of links and without any expanded data, like search results.\n\nUse `expanded` to have LHS expand that data, by performing necessary requests in parallel:\n\n```ruby\n# app/controllers/some_controller.rb\n\nSearch.where(what: 'Cafe').expanded\n```\n```\nGET https://service.example.com/search?what=Cafe\n{...\n  \"items\" : [\n    {\"href\": \"https://service.example.com/records/1\"},\n    {\"href\": \"https://service.example.com/records/2\"},\n    {\"href\": \"https://service.example.com/records/3\"}\n  ]\n}\n\nIn parallel:\n  \u003e GET https://service.example.com/records/1\n  \u003c {... name: 'Cafe Einstein'}\n  \u003e GET https://service.example.com/records/2\n  \u003c {... name: 'Starbucks'}\n  \u003e GET https://service.example.com/records/3\n  \u003c {... name: 'Plaza Cafe'}\n\n{\n  ...\n  \"items\" : [\n    {\n      \"href\": \"https://service.example.com/records/1\",\n      \"name\": 'Cafe Einstein',\n      ...\n    },\n    {\n      \"href\": \"https://service.example.com/records/2\",\n      \"name\": 'Starbucks',\n      ...\n    },\n    {\n      \"href\": \"https://service.example.com/records/3\",\n      \"name\": 'Plaza Cafe',\n      ...\n    }\n  ]\n}\n```\n\nYou can also apply request options to `expanded`. Those options will be used to perform the additional requests to expand the data:\n\n```ruby\n# app/controllers/some_controller.rb\n\nSearch.where(what: 'Cafe').expanded(auth: { bearer: access_token })\n```\n\n#### Error handling with chains\n\nOne benefit of chains is lazy evaluation. But that also means they only get resolved when data is accessed. This makes it hard to catch errors with normal `rescue` blocks:\n\n```ruby\n# app/controllers/some_controller.rb\n\ndef show\n  @records = Record.where(color: blue) # returns a chain, nothing is resolved, no http requests are performed\nrescue =\u003e e\n  # never ending up here, because the http requests are actually performed in the view, when the query chain is resolved\nend\n```\n\n```ruby\n# app/views/some/view.haml\n\n= @records.each do |record| # .each resolves the query chain, leads to http requests beeing performed, which might raises an exception\n  = record.name\n```\n\nTo simplify error handling with chains, you can also chain error handlers to be resolved, as part of the chain.\n\nIf you need to render some different view in Rails based on an LHS error raised during rendering the view, please proceed as following:\n\n```ruby\n# app/controllers/some_controller.rb\n\ndef show\n  @records = Record\n    .rescue(LHC::Error, -\u003e(error){ rescue_from(error) })\n    .where(color: 'blue')\n  render 'show'\n  render_error if @error\nend\n\nprivate\n\ndef rescue_from(error)\n  @error = error\n  nil\nend\n\ndef render_error\n  self.response_body = nil # required to not raise AbstractController::DoubleRenderError\n  render 'error'\nend\n```\n```\n\u003e GET https://service.example.com/records?color=blue\n\u003c 406\n```\n\nIn case no matching error handler is found the error gets re-raised.\n\n-\u003e Read more about [LHC error types/classes](https://github.com/local-ch/lhc#exceptions)\n\nIf you want to inject values for the failing records, that might not have been found, you can inject values for them with error handlers:\n\n```ruby\n# app/controllers/some_controller.rb\n\ndata = Record\n  .rescue(LHC::Unauthorized, -\u003e(response) { Record.new(name: 'unknown') })\n  .find(1, 2, 3)\n\ndata[1].name # 'unknown'\n```\n```\nIn parallel:\n  \u003e GET https://service.example.com/records/1\n  \u003c 200\n  \u003e GET https://service.example.com/records/2\n  \u003c 400\n  \u003e GET https://service.example.com/records/3\n  \u003c 200\n```\n\n-\u003e Read more about [LHC error types/classes](https://github.com/local-ch/lhc#exceptions)\n\n**If an error handler returns `nil` an empty LHS::Record is returned, not `nil`!**\n\nIn case you want to ignore errors and continue working with `nil` in those cases,\nplease use `ignore`:\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecord = Record.ignore(LHC::NotFound).find_by(color: 'blue')\n\nrecord # nil\n```\n\n#### Resolve chains: fetch\n\nIn case you need to resolve a query chain in place, use `fetch`:\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecords = Record.where(color: 'blue').fetch\n```\n\n#### Add request options to a query chain: options\n\nYou can apply options to the request chain. Those options will be forwarded to the request performed by the chain/query:\n\n```ruby\n# app/controllers/some_controller.rb\n\noptions = { auth: { bearer: '123456' } } # authenticated with OAuth token\n\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nAuthenticatedRecord = Record.options(options)\n\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nblue_records = AuthenticatedRecord.where(color: 'blue')\n\n```\n```\nGET https://service.example.com/records?color=blue { headers: { 'Authentication': 'Bearer 123456' } }\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nAuthenticatedRecord.create(color: 'red')\n\n```\n```\nPOST https://service.example.com/records { body: '{ color: \"red\" }' }, headers: { 'Authentication': 'Bearer 123456' } }\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecord = AuthenticatedRecord.find(123)\n\n```\n```\nGET https://service.example.com/records/123 { headers: { 'Authentication': 'Bearer 123456' } }\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nauthenticated_record = record.options(options) # starting a new chain based on the found record\n\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nauthenticated_record.valid?\n\n```\n```\nPOST https://service.example.com/records/validate { body: '{...}', headers: { 'Authentication': 'Bearer 123456' } }\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nauthenticated_record.save\n```\n```\nPOST https://service.example.com/records { body: '{...}', headers: { 'Authentication': 'Bearer 123456' } }\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nauthenticated_record.destroy\n\n```\n```\nDELETE https://service.example.com/records/123 { headers: { 'Authentication': 'Bearer 123456' } }\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nauthenticated_record.update(name: 'Steve')\n\n```\n```\nPOST https://service.example.com/records/123 { body: '{...}', headers: { 'Authentication': 'Bearer 123456' } }\n```\n\n#### Control pagination within a query chain\n\n`page` sets the page that you want to request.\n\n`per` sets the amount of items requested per page.\n\n`limit` is an alias for `per`. **But without providing arguments, it resolves the query and provides the current response limit per page**\n\n```ruby\n# app/controllers/some_controller.rb\n\nRecord.page(3).per(20).where(color: 'blue')\n\n```\n```\nGET https://service.example.com/records?offset=40\u0026limit=20\u0026color=blue\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nRecord.page(3).per(20).where(color: 'blue')\n\n```\n```\nGET https://service.example.com/records?offset=40\u0026limit=20\u0026color=blue\n```\n\nThe applied pagination strategy depends on what's configured for the particular record: See [Record pagination](#record-pagination)\n\n### Record pagination\n\nYou can configure pagination on a per record base. \nLHS differentiates between the [pagination strategy](#pagination-strategy) (how items/pages are navigated and calculated) and [pagination keys](#pagination-keys) (how stuff is named and accessed).\n\n#### Pagination strategy\n\n##### Pagination strategy: offset (default)\n\nThe offset pagination strategy is LHS's default pagination strategy, so nothing needs to be (re-)configured.\n\nThe `offset` pagination strategy starts with 0 and offsets by the amount of items, thay you've already recived – typically `limit`.\n\n```ruby\n# app/models/record.rb\n\nclass Search \u003c LHS::Record\n  endpoint '{+service}/search'\nend\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nRecord.all\n\n```\n```\nGET https://service.example.com/records?limit=100\n{\n  items: [{...}, ...],\n  total: 300,\n  limit: 100,\n  offset: 0\n}\nIn parallel:\n  GET https://service.example.com/records?limit=100\u0026offset=100\n  GET https://service.example.com/records?limit=100\u0026offset=200\n```\n\n##### Pagination strategy: page\n\nIn comparison to the `offset` strategy, the `page` strategy just increases by 1 (page) and sends the next batch of items for the next page.\n\n```ruby\n# app/models/record.rb\n\nclass Search \u003c LHS::Record\n  configuration pagination_strategy: 'page', pagination_key: 'page'\n\n  endpoint '{+service}/search'\nend\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nRecord.all\n\n```\n```\nGET https://service.example.com/records?limit=100\n{\n  items: [{...}, ...],\n  total: 300,\n  limit: 100,\n  page: 1\n}\nIn parallel:\n  GET https://service.example.com/records?limit=100\u0026page=2\n  GET https://service.example.com/records?limit=100\u0026page=3\n```\n\n##### Pagination strategy: start\n\nIn comparison to the `offset` strategy, the `start` strategy indicates with which item the current page starts. \nTypically it starts with 1 and if you get 100 items per page, the next start is 101.\n\n```ruby\n# app/models/record.rb\n\nclass Search \u003c LHS::Record\n  configuration pagination_strategy: 'start', pagination_key: 'startAt'\n\n  endpoint '{+service}/search'\nend\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nRecord.all\n\n```\n```\nGET https://service.example.com/records?limit=100\n{\n  items: [{...}, ...],\n  total: 300,\n  limit: 100,\n  page: 1\n}\nIn parallel:\n  GET https://service.example.com/records?limit=100\u0026startAt=101\n  GET https://service.example.com/records?limit=100\u0026startAt=201\n```\n\n##### Pagination strategy: link\n\nThe `link` strategy continuously follows in-response embedded links to following pages until the last page is reached (indicated by no more `next` link).\n\n*WARNING*\n\nLoading all pages from a resource paginated with links only can result in very poor performance, as pages can only be loaded sequentially!\n\n```ruby\n# app/models/record.rb\n\nclass Search \u003c LHS::Record\n  configuration pagination_strategy: 'link'\n\n  endpoint '{+service}/search'\nend\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nRecord.all\n\n```\n```\nGET https://service.example.com/records?limit=100\n{\n  items: [{...}, ...],\n  limit: 100,\n  next: {\n    href: 'https://service.example.com/records?from_record_id=p62qM5p0NK_qryO52Ze-eg\u0026limit=100'\n  }\n}\nSequentially:\n  GET https://service.example.com/records?from_record_id=p62qM5p0NK_qryO52Ze-eg\u0026limit=100\n  GET https://service.example.com/records?from_record_id=xcaoXBmuMyFFEcFDSgNgDQ\u0026limit=100\n```\n\n#### Pagination keys\n\n##### limit_key\n\n`limit_key` sets the key used to indicate how many items you want to retrieve per page e.g. `size`, `limit`, etc.\nIn case the `limit_key` parameter differs for how it needs to be requested from how it's provided in the response, use `body` and `parameter` subkeys.\n\n```ruby\n# app/models/record.rb\n\nclass Record \u003c LHS::Record\n  configuration limit_key: { body: [:pagination, :max], parameter: :max }\n\n  endpoint '{+service}/records'\nend\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecords = Record.where(color: 'blue')\nrecords.limit # 20\n```\n```\nGET https://service.example.com/records?color=blue\u0026max=100\n{ ...\n  items: [...],\n  pagination: { max: 20 }\n}\n```\n\n##### pagination_key\n\n`pagination_key` defines which key to use to paginate a page (e.g. `offset`, `page`, `startAt` etc.).\nIn case the `limit_key` parameter differs for how it needs to be requested from how it's provided in the response, use `body` and `parameter` subkeys.\n\n```ruby\n# app/models/record.rb\n\nclass Record \u003c LHS::Record\n  configuration pagination_key: { body: [:pagination, :page], parameter: :page }, pagination_strategy: :page\n\n  endpoint '{+service}/records'\nend\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecords = Record.where(color: 'blue').all\nrecords.length # 300\n```\n```\nGET https://service.example.com/records?color=blue\u0026limit=100\n{... pagination: { page: 1 } }\nIn parallel:\n  GET https://service.example.com/records?color=blue\u0026limit=100\u0026page=2\n  {... pagination: { page: 2 } }\n  GET https://service.example.com/records?color=blue\u0026limit=100\u0026page=3\n  {... pagination: { page: 3 } }\n```\n\n##### total_key\n\n`total_key` defines which key to user for pagination to describe the total amount of remote items (e.g. `total`, `totalResults`, etc.).\n\n```ruby\n# app/models/record.rb\n\nclass Record \u003c LHS::Record\n  configuration total_key: [:pagination, :total]\n\n  endpoint '{+service}/records'\nend\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecords = Record.where(color: 'blue').fetch\nrecords.length # 100\nrecords.count # 300\n```\n```\nGET https://service.example.com/records?color=blue\u0026limit=100\n{... pagination: { total: 300 } }\n```\n\n#### Pagination links\n\n##### next?\n\n`next?` Tells you if there is a next link or not.\n\n```ruby\n# app/controllers/some_controller.rb\n\n@records = Record.where(color: 'blue').fetch\n```\n```\nGET https://service.example.com/records?color=blue\u0026limit=100\n{... items: [...], next: 'https://service.example.com/records?color=blue\u0026limit=100\u0026offset=100' }\n```\n\n```ruby\n# app/views/some_view.haml\n\n- if @records.next?\n  = render partial: 'next_arrow'\n```\n\n##### previous?\n\n`previous?` Tells you if there is a previous link or not.\n\n```ruby\n# app/controllers/some_controller.rb\n\n@records = Record.where(color: 'blue').fetch\n```\n```\nGET https://service.example.com/records?color=blue\u0026limit=100\n{... items: [...], previous: 'https://service.example.com/records?color=blue\u0026limit=100\u0026offset=100' }\n```\n\n```ruby\n# app/views/some_view.haml\n\n- if @records.previous?\n  = render partial: 'previous_arrow'\n```\n\n#### Kaminari support (limited)\n\nLHS implements an interface that makes it partially working with Kaminari.\n\nThe kaminari’s page parameter is in params[:page]. For example, you can use kaminari to render paginations based on LHS Records. Typically, your code will look like this:\n\n```ruby\n# controller\n@items = Record.page(params[:page]).per(100)\n```\n\n```ruby\n# view\n= paginate @items\n```\n\n### Build, create and update records\n\n#### Create new records\n\n##### create\n\n`create` will return the object in memory if persisting fails, providing validation errors in `.errors` (See [record validation](#record-validation)).\n\n`create!` instead will raise an exception.\n\n`create` always builds the data of the local object first, before it tries to sync with an endpoint. So even if persisting fails, the local object is build.\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecord = Record.create(\n  text: 'Hello world'\n)\n\n```\n```\nPOST https://service.example.com/records { body: \"{ 'text' : 'Hello world' }\" }\n```\n\n-\u003e See [record validation](#record-validation) for how to handle validation errors when creating records.\n\n###### Unwrap nested data when creation response nests created record data\n\n`item_created_key` key used to merge record data thats nested in the creation response body:\n\n```ruby\n# app/models/location.rb\n\nclass Location \u003c LHS::Record\n\n  configuration item_created_key: [:response, :location]\n\nend\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nlocation.create(lat: '47.3920152', long: '8.5127981')\nlocation.address # Förrlibuckstrasse 62, 8005 Zürich\n```\n```\nPOST https://service.example.com/locations { body: \"{ 'lat': '47.3920152', long: '8.5127981' }\" }\n{... { response: { location: {... address: 'Förrlibuckstrasse 62, 8005 Zürich' } } } } \n```\n\n###### Create records through associations: Nested sub resources\n\n```ruby\n# app/models/restaurant.rb\n\nclass Restaurant \u003c LHS::Record\n  endpoint '{+service}/restaurants/{id}'\nend\n\n```\n\n```ruby\n# app/models/feedback.rb\n\nclass Feedback \u003c LHS::Record\n  endpoint '{+service}/restaurants/{restaurant_id}/feedbacks'\nend\n\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nrestaurant = Restaurant.find(1)\n```\n```\nGET https://service.example.com/restaurants/1\n{... reviews: { href: 'https://service.example.com/restaurants/1/reviews' }}\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nrestaurant.reviews.create(\n  text: 'Simply awesome!'\n)\n```\n```\nPOST https://service.example.com/restaurants/1/reviews { body: \"{ 'text': 'Simply awesome!' }\" }\n```\n\n#### Start building new records\n\nWith `new` or `build` you can start building new records from scratch, which can be persisted with `save`:\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecord = Record.new # or Record.build\nrecord.name = 'Starbucks'\nrecord.save\n```\n```\nPOST https://service.example.com/records { body: \"{ 'name' : 'Starbucks' }\" }\n```\n\n#### Change/Update existing records\n\n##### save\n\n`save` persist the whole object in its current state. \n\n`save` will return `false` if persisting fails. `save!` instead will raise an exception.\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecord = Record.find('1z-5r1fkaj')\n\n```\n```\nGET https://service.example.com/records/1z-5r1fkaj\n{ name: 'Starbucks', recommended: null }\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecord.recommended = true\nrecord.save\n\n```\n```\nPOST https://service.example.com/records/1z-5r1fkaj { body: \"{ 'name': 'Starbucks', 'recommended': true }\" }\n```\n\n-\u003e See [record validation](#record-validation) for how to handle validation errors when updating records.\n\n##### update\n\n###### Directly via Record\n\n```ruby\n# app/controllers/some_controller.rb\n\nRecord.update(id: '1z-5r1fkaj', name: 'Steve')\n\n```\n```\nGET https://service.example.com/records/1z-5r1fkaj\n{ name: 'Steve' }\n```\n\n###### per Instance\n\n`update` persists the whole object after new parameters are applied through arguments.\n\n`update` will return false if persisting fails. `update!` instead will raise an exception.\n\n`update` always updates the data of the local object first, before it tries to sync with an endpoint. So even if persisting fails, the local object is updated.\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecord = Record.find('1z-5r1fkaj')\n\n```\n```\nGET https://service.example.com/records/1z-5r1fkaj\n{ name: 'Starbucks', recommended: null }\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecord.update(recommended: true)\n\n```\n```\nPOST https://service.example.com/records/1z-5r1fkaj { body: \"{ 'name': 'Starbucks', 'recommended': true }\" }\n```\n\n-\u003e See [record validation](#record-validation) for how to handle validation errors when updating records.\n\nYou can use `update` and the end of query-chains:\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecord.options(method: :put).update(recommended: true)\n\n```\n\nYou can also pass explicit request options to `update`, by passing two explicit hashes:\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecord.update({ recommended: true }, { method: 'put' })\n\n```\n\n##### partial_update\n\n`partial_update` updates just the provided parameters.\n\n`partial_update` will return false if persisting fails. `partial_update!` instead will raise an exception.\n\n`partial_update` always updates the data of the local object first, before it tries to sync with an endpoint. So even if persisting fails, the local object is updated.\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecord = Record.find('1z-5r1fkaj')\n\n```\n```\nGET https://service.example.com/records/1z-5r1fkaj\n{ name: 'Starbucks', recommended: null }\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecord.partial_update(recommended: true)\n\n```\n```\nPOST https://service.example.com/records/1z-5r1fkaj { body: \"{ 'recommended': true }\" }\n```\n\n-\u003e See [record validation](#record-validation) for how to handle validation errors when updating records.\n\nYou can use `partial_update` at the end of query-chains:\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecord.options(method: :put).partial_update(recommended: true)\n\n```\n\nYou can also pass explicit request options to `partial_update`, by passing two explicit hashes:\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecord.partial_update({ recommended: true }, { method: 'put' })\n\n```\n\n#### Endpoint url parameter injection during record creation/change\n\nLHS injects parameters provided to `create`, `update`, `partial_update`, `save` etc. into an endpoint's URL when matching:\n\n```ruby\n# app/models/feedback.rb\n\nclass Feedback \u003c\u003c LHS::Record\n  endpoint '{+service}/records/{record_id}/feedbacks'\nend\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nFeedback.create(record_id: 51232, text: 'Great Restaurant!')\n```\n```\nPOST https://service.example.com/records/51232/feedbacks { body: \"{ 'text' : 'Great Restaurant!' }\" }\n```\n\n#### Record validation\n\nIn order to validate records before persisting them, you can use the `valid?` (`validate` alias) method.\n\nIt's **not recommended** to validate records anywhere, including application side validation via `ActiveModel::Validations`, except, if you validate them via the same endpoint/service, that also creates them.\n\nThe specific endpoint has to support validations without persistence. An endpoint has to be enabled (opt-in) in your record configurations:\n\n```ruby\n# app/models/user.rb\n\nclass User \u003c LHS::Record\n\n  endpoint '{+service}/users', validates: { params: { persist: false } }\n\nend\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nuser = User.build(email: 'i\\'m not an email address')\n\nunless user.valid?\n  @errors = user.errors\n  render 'new' and return\nend\n```\n```\nPOST https://service.example.com/users?persist=false { body: '{ \"email\" : \"i'm not an email address\"}' }\n{ \n  \"field_errors\": [{\n    \"path\": [\"email\"],\n    \"code\": \"WRONG_FORMAT\",\n    \"message\": \"The property value's format is incorrect.\"\n  }],\n  \"message\": \"Email must have the correct format.\"\n}\n```\n\nThe functionalities of `LHS::Errors` pretty much follow those of `ActiveModel::Validation`:\n\n```ruby\n# app/views/some_view.haml\n\n@errors.any? # true\n@errors.include?(:email) # true\n@errors[:email] # ['WRONG_FORMAT']\n@errors.messages # {:email=\u003e[\"Translated error message that this value has the wrong format\"]}\n@errors.codes # {:email=\u003e[\"WRONG_FORMAT\"]}\n@errors.message # Email must have the correct format.\"\n```\n\n##### Configure record validations\n\nThe parameters passed to the `validates` endpoint option are used to perform record validations:\n\n```ruby\n# app/models/user.rb\n\nclass User \u003c LHS::Record\n\n  endpoint '{+service}/users', validates: { params: { persist: false } }  # will add ?persist=false to the request\n  endpoint '{+service}/users', validates: { params: { publish: false } }  # will add ?publish=false to the request\n  endpoint '{+service}/users', validates: { params: { validates: true } } # will add ?validates=true to the request\n  endpoint '{+service}/users', validates: { path: 'validate' }            # will perform a validation via ...users/validate\n\nend\n```\n\n##### HTTP Status Codes for validation errors\n\nThe HTTP status code received from the endpoint when performing validations on a record, is available through the errors object:\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecord.save\nrecord.errors.status_code # 400\n```\n\n##### Reset validation errors\n\nClear the error messages like:\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecord.errors.clear\n```\n\n##### Add validation errors\n\nIn case you want to add application side validation errors, even though it's not recommended, do it as following:\n\n```ruby\nuser.errors.add(:name, 'WRONG_FORMAT')\n```\n\n##### Validation errors for nested data\n\nIf you work with complex data structures, you sometimes need to have validation errors delegated/scoped to nested data.\n\nThis features makes `LHS::Record`s compatible with how Rails or Simpleform renders/builds forms and especially error messages:\n\n```ruby\n# app/controllers/some_controller.rb\n\nunless @customer.save\n  @errors = @customer.errors\nend\n```\n```\nPOST https://service.example.com/customers { body: \"{ 'address' : { 'street': 'invalid', housenumber: '' } }\" }\n{ \n  \"field_errors\": [{\n    \"path\": [\"address\", \"street\"],\n    \"code\": \"REQUIRED_PROPERTY_VALUE_INCORRECT\",\n    \"message\": \"The property value is incorrect.\"\n  },{\n    \"path\": [\"address\", \"housenumber\"],\n    \"code\": \"REQUIRED_PROPERTY_VALUE\",\n    \"message\": \"The property value is required.\"\n  }],\n  \"message\": \"Some data is invalid.\"\n}\n```\n\n```ruby\n# app/views/some_view.haml\n\n= form_for @customer, as: :customer do |customer_form|\n\n  = fields_for 'customer[:address]', @customer.address, do |address_form|\n\n    = fields_for 'customer[:address][:street]', @customer.address.street, do |street_form|\n\n      = street_form.input :name\n      = street_form.input :house_number\n```\n\nThis would render nested forms and would also render nested form errors for nested data structures.\n\nYou can also access those nested errors like:\n\n```ruby\n@customer.address.errors\n@customer.address.street.errors\n```\n\n##### Translation of validation errors\n\nIf a translation exists for one of the following translation keys, LHS will provide a translated error (also in the following order) rather than the plain error message/code, when building forms or accessing `@errors.messages`:\n\n```ruby\nlhs.errors.records.\u003crecord_name\u003e.attributes.\u003cattribute_name\u003e.\u003cerror_code\u003e\ne.g. lhs.errors.records.customer.attributes.name.unsupported_property_value\n\nlhs.errors.records.\u003crecord_name\u003e.\u003cerror_code\u003e\ne.g. lhs.errors.records.customer.unsupported_property_value\n\nlhs.errors.messages.\u003cerror_code\u003e\ne.g. lhs.errors.messages.unsupported_property_value\n\nlhs.errors.attributes.\u003cattribute_name\u003e.\u003cerror_code\u003e\ne.g. lhs.errors.attributes.name.unsupported_property_value\n\nlhs.errors.fallback_message\n\nlhs.errors.records.\u003crecord_name\u003e.attributes.\u003ccollection\u003e.\u003cattribute_name\u003e.\u003cerror_code\u003e\ne.g. lhs.errors.records.appointment_proposal.attributes.appointments.date_time.date_property_not_in_future\n```\n\n##### Validation error types: errors vs. warnings\n\n###### Persistance failed: errors\n\nIf an endpoint returns errors in the response body, that is enough to interpret it as: persistance failed.\nThe response status code in this scenario is neglected.\n\n###### Persistance succeeded: warnings\n\nIn some cases, you need non blocking meta information about potential problems with the created record, so called warnings.\n\nIf the API endpoint implements warnings, returned when validating, they are provided just as `errors` (same interface and methods) through the `warnings` attribute:\n\n```ruby\n# app/controllres/some_controller.rb\n\n@presence = Presence.options(params: { synchronize: false }).create(\n  place: { href: 'http://storage/places/1' }\n)\n```\n```\nPOST https://service.example.com/presences { body: '{ \"place\": { \"href\": \"http://storage/places/1\" } }' }\n{\n    field_warnings: [{\n      code: 'WILL_BE_RESIZED',\n      path: ['place', 'photos', 0],\n      message: 'This photo is too small and will be resized.'\n    }\n  }\n```\n\n```ruby\n\npresence.warnings.any? # true\npresence.place.photos[0].warnings.messages.first # 'This photo is too small and will be resized.'\n\n```\n\n##### Using `ActiveModel::Validations` none the less\n\nIf you are using `ActiveModel::Validations`, even though it's not recommended, and you add errors to the LHS::Record instance, then those errors will be overwritten by the errors from `ActiveModel::Validations` when using `save`  or `valid?`. \n\nSo in essence, mixing `ActiveModel::Validations` and LHS built-in validations (via endpoints), is not compatible, yet.\n\n[Open issue](https://github.com/local-ch/lhs/issues/159)\n\n#### Use form_helper to create and update records\n\nRails `form_for` view-helper can be used in combination with instances of `LHS::Record`s to autogenerate forms:\n\n```ruby\n\u003c%= form_for(@instance, url: '/create') do |f| %\u003e\n  \u003c%= f.text_field :name %\u003e\n  \u003c%= f.text_area :text %\u003e\n  \u003c%= f.submit \"Create\" %\u003e\n\u003c% end %\u003e\n```\n\n### Destroy records\n\n`destroy`  deletes a record.\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecord = Record.find('1z-5r1fkaj')\n```\n```\nGET https://service.example.com/records/1z-5r1fkaj\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecord.destroy\n```\n```\nDELETE https://service.example.com/records/1z-5r1fkaj\n```\n\nYou can also destroy records directly without fetching them first:\n\n```ruby\n# app/controllers/some_controller.rb\n\ndestroyed_record = Record.destroy('1z-5r1fkaj')\n```\n```\nDELETE https://service.example.com/records/1z-5r1fkaj\n```\n\nor with parameters:\n\n```ruby\n# app/controllers/some_controller.rb\n\ndestroyed_records = Record.destroy(name: 'Steve')\n```\n```\nDELETE https://service.example.com/records?name='Steve'\n```\n\n### Record getters and setters\n\nSometimes it is necessary to implement custom getters and setters and convert data to a processable (endpoint) format behind the scenes.\n\n#### Record setters\n\nYou can define setter methods in `LHS::Record`s that will be used by initializers (`new`) and setter methods, that convert data provided, before storing it in the record and persisting it with a remote endpoint:\n\n```ruby\n# app/models/user.rb\n\nclass Feedback \u003c LHS::Record\n\n  def ratings=(values)\n    super(\n      values.map { |k, v| { name: k, value: v } }\n    )\n  end\nend\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecord = Record.new(ratings: { quality: 3 })\nrecord.ratings # [{ :name=\u003e:quality, :value=\u003e3 }]\n```\n\nSetting attributes with other names:\n\n```ruby\n# app/models/booking.rb\n\nclass Booking \u003c LHS::Record\n\n  def appointments_attributes=(values)\n    self.appointments = values.map { |appointment| appointment[:id] }\n  end\nend\n```\n\nor \n\n```ruby\n# app/models/booking.rb\n\nclass Booking \u003c LHS::Record\n\n  def appointments_attributes=(values)\n    self[:appointments] = values.map { |appointment| appointment[:id] }\n  end\nend\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nbooking.update(params)\n```\n\n#### Record getters\n\nIf you implement accompanying getter methods, the whole data conversion would be internal only:\n\n```ruby\n# app/models/user.rb\n\nclass Feedback \u003c LHS::Record\n\n  def ratings=(values)\n    super(\n      values.map { |k, v| { name: k, value: v } }\n    )\n  end\n\n  def ratings\n    super.map { |r| [r[:name], r[:value]] }]\n  end\nend\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nrecord = Record.new(ratings: { quality: 3 })\nrecord.ratings # {:quality=\u003e3}\n```\n\n### Include linked resources (hyperlinks and hypermedia)\n\nIn a service-oriented architecture using [hyperlinks](https://en.wikipedia.org/wiki/Hyperlink)/[hypermedia](https://en.wikipedia.org/wiki/Hypermedia), records/resources can contain hyperlinks to other records/resources.\n\nWhen fetching records with LHS, you can specify in advance all the linked resources that you want to include in the results. \n\nWith `includes` LHS ensures that all matching and explicitly linked resources are loaded and merged (even if the linked resources are paginated).\n\nIncluding linked resources/records is heavily influenced by [https://guides.rubyonrails.org/active_record_querying.html](https://guides.rubyonrails.org/active_record_querying.html#eager-loading-associations) and you should read it to understand this feature in all it's glory.\n\n#### Generate links from parameters\n\nSometimes you need to generate full hrefs/urls for records but you just have parameters that describe that record, like the ID.\n\nFor those usecases you can use `href_for(params)`:\n\n```ruby\n# app/controllers/some_controller.rb\n\nPresence.create(place: { href: Place.href_for(123) })\n```\n```\nPOST '/presences' { place: { href: \"http://datastore/places/123\" } }\n```\n\n#### Ensure the whole linked collection is included with includes\n\nIn case endpoints are paginated and you are certain that you'll need all objects of a set and not only the first page/batch, use `includes`.\n\nLHS will ensure that all linked resources are around by loading all pages (parallelized/performance optimized).\n\n```ruby\n# app/controllers/some_controller.rb\n\ncustomer = Customer.includes(contracts: :products).find(1)\n```\n```\n\u003e GET https://service.example.com/customers/1\n\u003c {... contracts: { href: 'https://service.example.com/customers/1/contracts' } }\n\u003e GET https://service.example.com/customers/1/contracts?limit=100\n\u003c {... items: [...], limit: 10, offset: 0, total: 32 }\nIn parallel: \n  \u003e GET https://service.example.com/customers/1/contracts?limit=10\u0026offset=10\n  \u003c {... products: [{ href: 'https://service.example.com/product/LBC' }] }\n  \u003e GET https://service.example.com/customers/1/contracts?limit=10\u0026offset=20\n  \u003c {... products: [{ href: 'https://service.example.com/product/LBB' }] }\nIn parallel:\n  \u003e GET https://service.example.com/product/LBC\n  \u003c {... name: 'Local Business Card' }\n  \u003e GET https://service.example.com/product/LBB\n  \u003c {... name: 'Local Business Basic' }\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\ncustomer.contracts.length # 32\ncustomer.contracts.first.products.first.name # Local Business Card\n\n```\n\n#### Include only the first linked page of a linked collection: includes_first_page\n\n`includes_first_page` includes the first page/response when loading the linked resource. **If the endpoint is paginated, only the first page will be included.**\n\n```ruby\n# app/controllers/some_controller.rb\n\ncustomer = Customer.includes_first_page(contracts: :products).find(1)\n```\n```\n\u003e GET https://service.example.com/customers/1\n\u003c {... contracts: { href: 'https://service.example.com/customers/1/contracts' } }\n\u003e GET https://service.example.com/customers/1/contracts?limit=100\n\u003c {... items: [...], limit: 10, offset: 0, total: 32 }\nIn parallel:\n  \u003e GET https://service.example.com/product/LBC\n  \u003c {... name: 'Local Business Card' }\n  \u003e GET https://service.example.com/product/LBB\n  \u003c {... name: 'Local Business Basic' }\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\ncustomer.contracts.length # 10\ncustomer.contracts.first.products.first.name # Local Business Card\n\n```\n\n#### Include various levels of linked data\n\nThe method syntax of `includes` allows you to include hyperlinks stored in deep nested data structures:\n\nSome examples:\n\n```ruby\nRecord.includes(:localch_account, :entry)\n# Includes localch_account -\u003e entry\n# { localch_account: { href: '...', entry: { href: '...' } } }\n\nRecord.includes([:localch_account, :entry])\n# Includes localch_account and entry\n# { localch_account: { href: '...' }, entry: { href: '...' } }\n\nRecord.includes(campaign: [:entry, :user])\n# Includes campaign and entry and user from campaign\n# { campaign: { href: '...' , entry: { href: '...' }, user: { href: '...' } } }\n```\n\n#### Identify and cast known records when including records\n\nWhen including linked resources with `includes`, already defined records and their endpoints and configurations are used to make the requests to fetch the additional data.\n\nThat also means that options for endpoints of linked resources are applied when requesting those in addition.\n\nThis applies for example a records endpoint configuration even though it's fetched/included through another record:\n\n```ruby\n# app/models/favorite.rb\n\nclass Favorite \u003c LHS::Record\n\n  endpoint '{+service}/users/{user_id}/favorites', auth: { basic: { username: 'steve', password: 'can' } }\n  endpoint '{+service}/users/{user_id}/favorites/:id', auth: { basic: { username: 'steve', password: 'can' } }\n\nend\n```\n\n```ruby\n# app/models/place.rb\n\nclass Place \u003c LHS::Record\n\n  endpoint '{+service}/v2/places', auth: { basic: { username: 'steve', password: 'can' } }\n  endpoint '{+service}/v2/places/{id}', auth: { basic: { username: 'steve', password: 'can' } }\n\nend\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nFavorite.includes(:place).where(user_id: current_user.id)\n\n```\n```\n\u003e GET https://service.example.com/users/123/favorites { headers: { 'Authentication': 'Basic c3RldmU6Y2Fu' } }\n\u003c {... items: [... { place: { href: 'https://service.example.com/place/456' } } ] }\nIn parallel:\n  \u003e GET https://service.example.com/place/456 { headers: { 'Authentication': 'Basic c3RldmU6Y2Fu' } }\n  \u003e GET https://service.example.com/place/789 { headers: { 'Authentication': 'Basic c3RldmU6Y2Fu' } }\n  \u003e GET https://service.example.com/place/1112 { headers: { 'Authentication': 'Basic c3RldmU6Y2Fu' } }\n  \u003e GET https://service.example.com/place/5423 { headers: { 'Authentication': 'Basic c3RldmU6Y2Fu' } }\n```\n\n#### Apply options for requests performed to fetch included records\n\nUse `references` to apply request options to requests performed to fetch included records:\n\n```ruby\n# app/controllers/some_controller.rb\n\nFavorite.includes(:place).references(place: { auth: { bearer: '123' }}).where(user_id: 1)\n```\n```\nGET https://service.example.com/users/1/favorites\n{... items: [... { place: { href: 'https://service.example.com/places/2' } }] }\nIn parallel:\n  GET https://service.example.com/places/2 { headers: { 'Authentication': 'Bearer 123' } }\n  GET https://service.example.com/places/3 { headers: { 'Authentication': 'Bearer 123' } }\n  GET https://service.example.com/places/4 { headers: { 'Authentication': 'Bearer 123' } }\n```\n\nHere is another example, if you want to ignore errors, that occur while you fetch included resources:\n\n```ruby\n# app/controllers/some_controller.rb\n\nfeedback = Feedback\n  .includes(campaign: :entry)\n  .references(campaign: { ignore: LHC::NotFound })\n  .find(12345)\n```\n\n#### compact: Remove included resources that didn't return any records\n\nIn case you include nested data and ignored errors while including, it can happen that you get back a collection that contains data based on response errors:\n\n```ruby\n# app/controllers/some_controller.rb\n\nuser = User\n  .includes(:places)\n  .references(places: { ignore: LHC::NotFound })\n  .find(123)\n```\n\n```\nGET http://service/users/123\n{ \"places\": { \"href\": \"http://service/users/123/places\" } }\n\nGET http://service/users/123/places\n{ \"items\": [\n  { \"href\": \"http://service/places/1\" },\n  { \"href\": \"http://service/places/2\" }\n] }\n\nGET http://service/places/1\n200 { \"name\": \"Casa Ferlin\" }\n\nGET http://service/places/2\n404 { \"status\": 404, \"error\": \"not found\" }\n```\n\n```ruby\nuser.places[1] # { \"status\": 404, \"error\": \"not found\" }\n```\n\nIn order to exclude items from a collection which where not based on successful responses, use `.compact` or `.compact!`:\n\n```ruby\n# app/controllers/some_controller.rb\n\nuser = User\n  .includes(:places)\n  .references(places: { ignore: LHC::NotFound })\n  .find(123)\nplaces = user.places.compact\n\nplaces # { \"items\": [ { \"href\": \"http://service/places/1\", \"name\": \"Casa Ferlin\" } ] }\n```\n\n### Record batch processing\n\n**Be careful using methods for batch processing. They could result in a lot of HTTP requests!**\n\n#### all\n\n`all` fetches all records from the service by doing multiple requests, best-effort parallelization, and resolving endpoint pagination if necessary:\n\n```ruby\nrecords = Record.all\n```\n```\n\u003e GET https://service.example.com/records?limit=100\n\u003c {...\n  items: [...]\n  total: 900,\n  limit: 100,\n  offset: 0\n}\nIn parallel:\n  \u003e GET https://service.example.com/records?limit=100\u0026offset=100\n  \u003e GET https://service.example.com/records?limit=100\u0026offset=200\n  \u003e GET https://service.example.com/records?limit=100\u0026offset=300\n  \u003e GET https://service.example.com/records?limit=100\u0026offset=400\n  \u003e GET https://service.example.com/records?limit=100\u0026offset=500\n  \u003e GET https://service.example.com/records?limit=100\u0026offset=600\n  \u003e GET https://service.example.com/records?limit=100\u0026offset=700\n  \u003e GET https://service.example.com/records?limit=100\u0026offset=800\n```\n\n`all` is chainable and has the same interface like `where`:\n\n```ruby\nRecord.where(color: 'blue').all\nRecord.all.where(color: 'blue')\nRecord.all(color: 'blue')\n```\n\nAll three are doing the same thing: fetching all records with the color 'blue' from the endpoint while resolving pagingation if endpoint is paginated.\n\n##### Using all, when endpoint does not implement response pagination meta data\n\nIn case an API does not provide pagination information in the repsponse data (limit, offset and total), LHS keeps on loading pages when requesting `all` until the first empty page responds.\n\n#### find_each\n\n`find_each` is a more fine grained way to process single records that are fetched in batches.\n\n```ruby\nRecord.find_each(start: 50, batch_size: 20, params: { has_reviews: true }, headers: { 'Authorization': 'Bearer 123' }) do |record|\n  # Iterates over each record. Starts with record no. 50 and fetches 20 records each batch.\n  record\n  break if record.some_attribute == some_value\nend\n```\n\n#### find_in_batches\n\n`find_in_batches` is used by `find_each` and processes batches.\n\n```ruby\nRecord.find_in_batches(start: 50, batch_size: 20, params: { has_reviews: true }, headers: { 'Authorization': 'Bearer 123' }) do |records|\n  # Iterates over multiple records (batch size is 20). Starts with record no. 50 and fetches 20 records each batch.\n  records\n  break if records.first.name == some_value\nend\n```\n\n### Convert/Cast specific record types: becomes\n\nBased on [ActiveRecord's implementation](https://api.rubyonrails.org/classes/ActiveRecord/Persistence.html#method-i-becomes), LHS implements `becomes`, too.\n\nIt's a way to convert records of a certain type A to another certain type B.\n\n_NOTE: RPC-style actions, that are discouraged in REST anyway, are utilizable with this functionality, too. See the following example:_\n\n```ruby\n# app/models/location.rb\n\nclass Location \u003c LHS::Record\n  endpoint '{+service}/locations'\n  endpoint '{+service}/locations/{id}'\nend\n```\n\n```ruby\n# app/models/synchronization.rb\n\nclass Synchronization \u003c LHS::Record\n  endpoint '{+service}/locations/{id}/sync'\nend\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nlocation = Location.find(1)\n```\n```\nGET https://service.example.com/location/1\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\nsynchronization = location.becomes(Synchronization)\nsynchronization.save!\n```\n```\nPOST https://service.example.com/location/1/sync { body: '{ ... }' }\n```\n\n### Assign attributes\n\nAllows you to set the attributes by passing in a hash of attributes.\n\n```ruby\nentry = LocalEntry.new\nentry.assign_attributes(company_name: 'localsearch')\nentry.company_name # =\u003e 'localsearch'\n```\n\n## Request Cycle Cache\n\nBy default, LHS does not perform the same http request multiple times during one request/response cycle.\n\n```ruby\n# app/models/user.rb\n\nclass User \u003c LHS::Record\n  endpoint '{+service}/users/{id}'\nend\n```\n\n```ruby\n# app/models/location.rb\n\nclass Location \u003c LHS::Record\n  endpoint '{+service}/locations/{id}'\nend\n```\n\n```ruby\n# app/controllers/some_controller.rb\n\ndef index\n  @user = User.find(1)\n  @locations = Location.includes(:owner).find(2)\nend\n```\n```\nGET https://service.example.com/users/1\nGET https://service.example.com/location/2\n{... owner: { href: 'https://service.example.com/users/1' } }\nFrom cache:\n  GET https://service.example.com/users/1\n```\n\nIt uses the [LHC Caching Interceptor](https://github.com/local-ch/lhc#caching-interceptor) as caching mechanism base and sets a unique request id for every request cycle with Railties to ensure data is just cached within one request cycle and not shared with other requests.\n\nOnly GET requests are considered for caching by using LHC Caching Interceptor's `cache_methods` option internally and considers request headers when caching requests, so requests with different headers are not served from cache.\n\nThe LHS Request Cycle Cache is opt-out, so it's enabled by default and will require you to enable the [LHC Caching Interceptor](https://github.com/local-ch/lhc#caching-interceptor) in your project.\n\n### Change store for LHS' request cycle cache\n\nBy default the LHS Request Cycle Cache will use `ActiveSupport::Cache::MemoryStore` as its cache store. Feel free to configure a cache that is better suited for your needs by:\n\n```ruby\n# config/initializers/lhs.rb\n\nLHS.configure do |config|\n  config.request_cycle_cache = ActiveSupport::Cache::MemoryStore.new\nend\n```\n\n### Disable request cycle cache\n\nIf you want to disable the LHS Request Cycle Cache, simply disable it within configuration:\n\n```ruby\n# config/initializers/lhs.rb\n\nLHS.configure do |config|\n  config.request_cycle_cache_enabled = false\nend\n```\n\n## Automatic Authentication (OAuth)\n\nLHS provides a way to have records automatically fetch and use OAuth authentication when performing requests within Rails.\n\nIn order to enable automatic oauth authentication, perform the following steps:\n\n1. Make sure LHS is configured to perform `auto_oauth`. Provide a block that, when executed in the controller context, returns a valid access_token/bearer_token.\n```ruby\n# config/initializers/lhs.rb\n\nLHS.configure do |config|\n  config.auto_oauth = -\u003e { access_token }\nend\n```\n\n2. Opt-in records requiring oauth authentication:\n\n```ruby\n# app/models/record.rb\n\nclass Record \u003c LHS::Record\n  oauth\n  # ...\nend\n```\n\n3. Include the `LHS::OAuth` context into your application controller:\n\n```ruby\n# app/controllers/application_controller.rb\n\nclass ApplicationController \u003c ActionController::Base\n  include LHS::OAuth\n\n  # ...\nend\n```\n\n4. Make sure you have the `LHC::Auth` interceptor enabled:\n\n```ruby\n# config/initializers/lhc.rb\n\nLHC.configure do |config|\n  config.interceptors = [LHC::Auth]\nend\n```\n\nNow you can perform requests based on the record that will be auto authenticated from now on:\n\n```ruby\n# app/controllers/some_controller.rb\n\nRecord.find(1)\n```\n```\nhttps://records/1\nAuthentication: 'Bearer token-12345'\n```\n\n### Configure multiple auth providers (even per endpoint)\n\nIn case you need to configure multiple auth provider access_tokens within your application,\nmake sure you provide a proc returning a hash when configuring `auto_oauth`, \nnaming every single provider and the responsive method to retrieve the access_tokens in the controller context:\n\n```ruby\n# config/initializers/lhs.rb\nLHS.configure do |config|\n  config.auto_oauth = proc do\n    {\n      provider1: access_token_provider_1,\n      provider2: access_token_provider_2\n    }\n  end\nend\n```\n\nThen make sure you either define which provider to use on a record level:\n\n```ruby\n# model/record.rb\nclass Record \u003c LHS::Record\n  oauth(:provider1)\n  #...\nend\n```\n\nor on an endpoint level:\n\n```ruby\n# model/record.rb\nclass Record \u003c LHS::Record\n  endpoint 'https://service/records', oauth: :provider1\n  #...\nend\n```\n\n### Configure providers\n\nIf you're using LHS service providers, you can also configure auto auth on a provider level:\n\n```ruby\n# app/models/providers/localsearch.rb\nmodule Providers\n  class Localsearch \u003c LHS::Record\n    \n    provider(\n      oauth: true\n    )\n  end\nend\n```\n\nor with multiple auth providers:\n\n```ruby\n# app/models/providers/localsearch.rb\nmodule Providers\n  class Localsearch \u003c LHS::Record\n    \n    provider(\n      oauth: :provider_1\n    )\n  end\nend\n```\n\n## Option Blocks\n\nIn order to apply options to all requests performed in a give block, LHS provides option blocks.\n\n```ruby\n# app/controllers/records_controller.rb\n\nLHS.options(headers: { 'Tracking-Id' =\u003e 123 }) do\n  Record.find(1)\nend\n\nRecord.find(2)\n```\n```\nGET https://records/1 { headers: { 'Tracking-Id' =\u003e '123' } }\nGET https://records/2 { headers: { } }\n```\n\n## Request tracing\n\nLHS supports tracing the source (in your application code) of http requests being made with methods like `find find_by find_by! first first! last last!`.\n\nFollowing links, and using `includes` are not traced (just yet).\n\nIn order to enable tracing you need to enable it via LHS configuration:\n\n```ruby\n# config/initializers/lhs.rb\n\nLHS.configure do |config|\n  config.trace = Rails.env.development? || Rails.logger.level == 0 # debug\nend\n```\n\n```ruby\n# app/controllers/application_controller.rb\n\ncode = Code.find(code: params[:code])\n```\n```\nCalled from onboarding/app/controllers/concerns/access_code_concern.rb:11:in `access_code'\n```\n\nHowever, following links and includes won't get traced (just yet):\n\n```ruby\n# app/controllers/application_controller.rb\n\ncode = Code.includes(:places).find(123)\n```\n\n```\n# Nothing is traced\n{\n  places: [...]\n}\n```\n\n```ruby\ncode.places\n```\n```\n{ \n  token: \"XYZABCDEF\",\n  places:\n    [\n      { href: \"http://storage-stg.preprod-local.ch/v2/places/egZelgYhdlg\" }\n    ]\n}\n```\n\n## Extended Rollbar Logging\n\nIn order to log all requests/responses prior to an exception reported by Rollbar in addition to the exception itself, use the `LHS::ExtendedRollbar` interceptor in combination with the rollbar processor/handler:\n\n```ruby\n# config/initializers/lhc.rb\n\nLHC.configure do |config|\n  config.interceptors = [LHS::ExtendedRollbar]\nend\n```\n\n```ruby\n# config/initializers/rollbar.rb\n\nRollbar.configure do |config|\n  config.before_process \u003c\u003c LHS::Interceptors::ExtendedRollbar::Handler.init\nend\n```\n\n## Testing with LHS\n\n**Best practice in regards of testing applications using LHS, is to let LHS fetch your records, actually perform HTTP requests and [WebMock](https://github.com/bblimke/webmock) to stub/mock those http requests/responses.**\n\nThis follows the [Black Box Testing](https://en.wikipedia.org/wiki/Black-box_testing) approach and prevents you from creating constraints to LHS' internal structures and mechanisms, which will break as soon as we change internals.\n\n```ruby\n# specs/*/some_spec.rb \n\nlet(:contracts) do\n  [\n    {number: '1'},\n    {number: '2'},\n    {number: '3'}\n  ]\nend\n\nbefore do\n  stub_request(:get, \"https://service.example.com/contracts\")\n    .to_return(\n      body: {\n        items: contracts,\n        limit: 10,\n        total: contracts.length,\n        offset: 0\n      }.to_json\n    )\nend\n\nit 'displays contracts' do\n  visit 'contracts'\n  contracts.each do |contract|\n    expect(page).to have_content(contract[:number])\n  end\nend\n```\n\n### Test helper\n\nIn order to load LHS test helpers into your tests, add the following to your spec helper:\n\n```ruby\n# spec/spec_helper.rb\n\nrequire 'lhs/rspec'\n```\n\nThis e.g. will prevent running into caching issues during your tests, when (request cycle cache)[#request-cycle-cache] is enabled.\nIt will initialize a MemoryStore cache for LHC::Caching interceptor and resets the cache before every test.\n\n#### Stub\n\nLHS offers stub helpers that simplify stubbing https request to your apis through your defined Records.\n\n##### stub_all\n\n`Record.stub_all(url, items, additional_options)`\n\n```ruby\n# your_spec.rb\n\nbefore do\n  class Record \u003c LHS::Record\n    endpoint 'https://records'\n  end\n\n  Record.stub_all(\n    'https://records',\n    200.times.map{ |index| { name: \"Item #{index}\" } },\n    headers: {\n      'Authorization' =\u003e 'Bearer 123'\n    }\n  )\nend\n```\n```\nGET https://records?limit=100\nGET https://records?limit=100\u0026offset=100\n```\n\nLHS also uses Record configuration when stubbing all.\n```ruby\n# your_spec.rb\n\nbefore do\n  class Record \u003c LHS::Record\n    configuration limit_key: :per_page, pagination_strategy: :page, pagination_key: :page\n\n    endpoint 'https://records'\n  end\n\n  Record.stub_all(\n    'https://records',\n    200.times.map{ |index| { name: \"Item #{index}\" } }\n  )\nend\n```\n```\nGET https://records?per_page=100\nGET https://records?per_page=100\u0026page=2\n```\n\n### Test query chains\n\n#### By explicitly resolving the chain: fetch\n\nUse `fetch` in tests to resolve chains in place and expect WebMock stubs to be requested.\n\n```ruby\n# specs/*/some_spec.rb \n\nrecords = Record.where(color: 'blue').where(available: true).where(color: 'red')\n\nexpect(\n  records.fetch\n).to have_requested(:get, %r{records/})\n  .with(query: hash_including(color: 'blue', available: true))\n```\n\n#### Without resolving the chain: where_values_hash\n\nAs `where` chains are not resolving to HTTP-requests when no data is accessed, you can use `where_values_hash` to access the values that would be used to resolve the chain, and test those:\n\n```ruby\n# specs/*/some_spec.rb \n\nrecords = Record.where(color: 'blue').where(available: true).where(color: 'red')\n\nexpect(\n  records.where_values_hash\n).to eq {color: 'red', available: true}\n```\n\n## Extended developer documentation\n\n### Accessing data in LHS\n\n![diagram](docs/accessing-data-in-lhs.png)\n\n## License\n\n[GNU General Public License Version 3.](https://www.gnu.org/licenses/gpl-3.0.en.html)\n\n","funding_links":[],"categories":["Ruby"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flocal-ch%2Flhs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flocal-ch%2Flhs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flocal-ch%2Flhs/lists"}