{"id":15678217,"url":"https://github.com/manfred/reynard","last_synced_at":"2025-08-07T23:13:37.278Z","repository":{"id":42446955,"uuid":"194275597","full_name":"Manfred/Reynard","owner":"Manfred","description":"Minimal OpenAPI client for Ruby.","archived":false,"fork":false,"pushed_at":"2024-08-22T08:17:14.000Z","size":351,"stargazers_count":13,"open_issues_count":9,"forks_count":3,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-13T02:13:02.729Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Manfred.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"docs/CONTRIBUTING.md","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":"2019-06-28T13:07:54.000Z","updated_at":"2025-04-08T20:03:37.000Z","dependencies_parsed_at":"2023-11-20T22:24:01.951Z","dependency_job_id":"3fada94b-9964-40e1-841a-0ff78cdb72c3","html_url":"https://github.com/Manfred/Reynard","commit_stats":{"total_commits":157,"total_committers":1,"mean_commits":157.0,"dds":0.0,"last_synced_commit":"55997e98c867a8c2c65ae67455e0c7f9c100493f"},"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Manfred%2FReynard","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Manfred%2FReynard/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Manfred%2FReynard/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Manfred%2FReynard/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Manfred","download_url":"https://codeload.github.com/Manfred/Reynard/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248654094,"owners_count":21140236,"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":[],"created_at":"2024-10-03T16:19:14.866Z","updated_at":"2025-04-13T02:13:08.414Z","avatar_url":"https://github.com/Manfred.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Reynard\n\nReynard is an OpenAPI client for Ruby. It operates directly on the OpenAPI specification without the need to generate any source code.\n\n```ruby\n# A Client does not have a fixed state and creating a new\n# client will never incur a cost over creating the object\n# itself.\nreynard = Reynard.new(filename: 'openapi.yml')\n```\n\n## Installing\n\nReynard is distributed as a gem called `reynard`.\n\n## Choosing a server\n\nAn OpenAPI specification may specify multiple servers. There is no automated way to select the ‘correct’ server so Reynard uses the first one by default.\n\nFor example:\n\n```yaml\nservers:\n  - url: http://production.example.com/v1\n  - url: http://staging.example.com/v1\n```\n\nWill cause Reynard to choose the production URL.\n\n```ruby\nreynard.url #=\u003e \"http://production.example.com/v1\"\n```\n\nYou can override the `base_url` if you want to use a different one.\n\n```ruby\nreynard.base_url('http://test.example.com/v1')\n```\n\nYou also have access to all servers in the specification so you can automatically select one however you want.\n\n```ruby\nbase_url = reynard.servers.map(\u0026:url).find do |url|\n  /staging/.match(url)\nend\nreynard.base_url(base_url)\n```\n\n## Calling endpoints\n\nAssuming there is an operation with the `operationId` set to `employeeByUuid` you can perform a request as shown below. Note that `operationId` is a required property in the specs.\n\n```ruby\nresponse = reynard.\n  operation('employeeByUuid').\n  params(uuid: uuid).\n  execute\n```\n\nWhen an operation requires a body, you can add it as structured data. It will be converted to JSON automatically.\n\n```ruby\nresponse = reynard.\n  operation('createEmployee').\n  body(name: 'Sam Seven').\n  execute\n```\n\nThe response object shares much of its interface with `Net::HTTP::Response`.\n\n```ruby\nresponse.code #=\u003e '200'\nresponse.content_type #=\u003e 'application/json'\nresponse['Content-Type'] #=\u003e 'application/json'\nresponse.body #=\u003e '{\"name\":\"Sam Seven\"}'\nresponse.parsed_body #=\u003e { \"name\" =\u003e \"Sam Seven\" }\n```\n\nYou can test for groups of response codes, basically matching `1xx` through `5xx`.\n\n```ruby\nresponse.informational?\nresponse.success?\nresponse.redirection?\nresponse.client_error?\nresponse.server_error?\n```\n\nIn case the response status and content-type matches a response in the specification it will attempt to build an object using the specified schema.\n\n```ruby\nresponse.object.name #=\u003e 'Sam Seven'\n```\n\nSee below for more details about the object builder.\n\n## Schema and models\n\nReynard has an object builder that allows you to get a value object backed by model classes based on the resource schema.\n\nFor example, when the schema for a response is something like this:\n\n```yaml\nbook:\n  type: object\n  properties:\n    name:\n      type: string\n    author:\n      type: object\n      properties:\n        name:\n          type: string\n```\n\nAnd the parsed body from the response is:\n\n```json\n{\n  \"name\": \"Erebus\",\n  \"author\": { \"name\": \"Palin\" }\n}\n```\n\nYou should be able to access it using:\n\n```ruby\nresponse.object.class #=\u003e Reynard::Models::Book\nresponse.object.author.class #=\u003e Reynard::Models::Author\nresponse.object.author.name #=\u003e 'Palin'\n```\n\n### Model name\n\nModel names are determined in order:\n\n1. From the `title` attribute of a schema\n2. From the `$ref` pointing to the schema\n3. From the path to the definition of the schema\n\n```yaml\napplication/json:\n  schema:\n    $ref: \"#/components/schemas/Book\"\ncomponents:\n  schemas:\n    Book:\n      type: object\n      title: LibraryBook\n```\n\nIn this example it would use the `title` and the model name would be `LibraryBook`. Otherwise it would use `Book` from the end of the `$ref`.\n\nIf neither of those are available it would look at the full expanded path. \n\n```\nbooks:\n  type: array\n  items:\n    type: object\n```  \n\nFor example, in case of an array item it would look at `books` and singularize it to `Book`.\n\nIf you run into issues where Reynard doesn't properly build an object for a nested resource, it's probably because of a naming issue. It's advised to add a `title` property to the schema definition with a unique name in that case.\n\n### Properties and model attributes\n\nReynard provides access to JSON properties on the model in a number of ways. There are some restrictions because of Ruby, so it's good to understand them.\n\nLet's assume there is a payload for an `Author` model that looks like this:\n\n```json\n{\"first_name\":\"Marcél\",\"lastName\":\"Marcellus\",\"1st-class\":false}\n```\n\nReynard attemps to give access to these properties as much as possible by sanitizing and normalizing them, so you can do the following:\n\n```ruby\nresponse.object.first_name #=\u003e \"Marcél\"\nresponse.object.last_name #=\u003e \"Marcellus\"\n```\n\nBut it's also possible to use the original casing for `lastName`.\n\n```ruby\nresponse.object.lastName #=\u003e \"Marcellus\"\n```\n\nHowever, a method can't start with a number and can't contain dashes in Ruby so the following is not possible:\n\n```\n# Not valid Ruby syntax:\nresponse.object.1st-class\n```\n\nThere are two alternatives for accessing this property:\n\n```ruby\n# The preferred solution for accessing raw property values is through the\n# parsed JSON on the response object.\nresponse.parsed_body[\"1st-class\"]\n# When you are processing nested models and you don't have access to the\n# response object, you can choose to use the `[]` method.\nresponse.object[\"1st-class\"]\n# Don't use `send` to access the property, this may not work in future\n# versions.\nresponse.object.send(\"1st-class\")\n```\n\n#### Mapping properties\n\nIn case you are forced to access a property through a method, you could choose to map irregular property names to method names globally for all models:\n\n```ruby\nreynard.snake_cases({ \"1st-class\" =\u003e \"first_class\" })\n```\n\nThis will allow you to access the property through the `first_class` method without changing the behavior of the rest of the object.\n\n```ruby\nresponse.object.first_class #=\u003e false\nresponse.object[\"1st-class\"] #=\u003e false\n```\n\nDon't use this to map common property names that would work fine otherwise, because you could make things really confusing.\n\n```ruby\n# Don't do this.\nreynard.snake_cases({ \"name\" =\u003e \"naem\" })\n```\n\n### Optional properties\n\nThe current version of Reynard does not read or enforce the properties defined in the schema, instead it builds the response object based on the properties returned by the service. This was done deliberately to make it easier to access a server with a newer or older schema than the one used to build the Reynard instance.\n\nIn the code that means that you may have to check if you are receiving certain attributes, you can do this in a number of ways:\n\n```ruby\nresponse.object.respond_to?(:name)\nresponse.parsed_body[\"name\"]\nresponse.object[\"name\"]\n```\n\n### Taking control of a model\n\nAs noted earlier there is a deterministic way in which Reynard decides on a model name. This means that you can define the model name before Reynard gets to it.\n\nThe easiest way to find out how Reynard does this, is to actually perform the operation and look at the response. Let's look at an example where Reynard creates  a `Library` model:\n\n```ruby\nresponse.object.class #=\u003e Reynard::Models::Library\nresponse.parsed_body #=\u003e {\"name\" =\u003e \"Alexandria\"}\n```\n\nOne way to ensure that the response object has the required attributes is to defined a `valid?` method on it:\n\n\n```ruby\nclass Reynard\n  module Models\n    class Library \u003c Reynard::Model\n      def valid?\n        (%w[name] - @attributes.keys).empty?\n      end\n    end\n  end\nend\n```\n\nNext time you perform a request you can use your version of `Library`:\n\n```ruby\nif response.object.valid?\n  puts \"The library is valid!\"\nelse\n  puts \"The library is not valid :-( #{response.parsed_object.inspect}\"\nend\n```\n\nAnother way to do this is to override the `attributes=` method.\n\n```ruby\ndef attributes=(attributes)\n  super # call super or nested attributes and other features will break\n  raise_invalid unless valid?\nend\n\nprivate\n\ndef raise_invalid\n  return if valid?\n\n  raise(\n    ArgumentError,\n    \"Library may not be initialized without all required attributes.\"\n  )\nend\n```\n\nA third way of dealing with optional attributes is to define an accessor yourself.\n\n```ruby\ndef name\n  @attributes.fetch(\"name\") { \"Unnnamed library\" }\nend\n```\n\n## Logging\n\nWhen you want to know what the Reynard client is doing you can enable logging.\n\n```ruby\nlogger = Logger.new($stdout)\nlogger.level = Logger::INFO\nreynard.logger(logger).execute\n```\n\nThe logging should be compatible with the Ruby on Rails logger.\n\n```ruby\nreynard.logger(Rails.logger).execute\n```\n\n## Headers\n\nYou can add request headers at any time to a Reynard context, these are additive so you can easily have global headers for all requests and specific ones for an operation.\n\n```ruby\nreynard = reynard.headers(\n  {\n    \"User-Agent\" =\u003e \"MyApplication/12.1.1 Reynard/#{Reynard::VERSION}\",\n    \"Accept\" =\u003e \"application/json\"\n  }\n)\n```\n\n## Debugging\n\nYou can turn on debug logging in `Net::HTTP` by setting the `DEBUG` environment variable. After setting this, all HTTP interaction will be written to STDERR.\n\n```sh\nenv DEBUG=true ruby script.rb\n```\n\nInternally this will set `http.debug_output = $stderr` on the HTTP object in the client.\n\n## Mocking\n\nYou can mock Reynard requests by changing the HTTP implementation. The class **must** implement a single `request` method that accepts an URI and net/http request object. It **must** return a net/http response object or an object with the exact same interface.\n\n```ruby\nReynard.http = MyMock.new\n\nclass MyMock\n  def request(uri, net_http_request)\n    Net::HTTPResponse::CODE_TO_OBJ['404'].new('HTTP/1.1', '200', 'OK')\n  end\nend\n```\n\n## Copyright and other legal\n\nSee LICENCE.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmanfred%2Freynard","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmanfred%2Freynard","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmanfred%2Freynard/lists"}