{"id":13482894,"url":"https://github.com/balvig/spyke","last_synced_at":"2025-05-14T21:09:55.477Z","repository":{"id":21840983,"uuid":"25164099","full_name":"balvig/spyke","owner":"balvig","description":"Interact with REST services in an ActiveRecord-like manner","archived":false,"fork":false,"pushed_at":"2024-04-01T06:15:28.000Z","size":349,"stargazers_count":884,"open_issues_count":14,"forks_count":67,"subscribers_count":12,"default_branch":"main","last_synced_at":"2024-04-01T07:22:57.846Z","etag":null,"topics":["activerecord","api","faraday","json","rest","rest-api","ruby"],"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/balvig.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","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}},"created_at":"2014-10-13T15:18:54.000Z","updated_at":"2024-04-01T07:23:11.176Z","dependencies_parsed_at":"2024-04-01T07:23:09.811Z","dependency_job_id":"d524e357-f3b2-44d4-9728-c61add6dec86","html_url":"https://github.com/balvig/spyke","commit_stats":{"total_commits":297,"total_committers":22,"mean_commits":13.5,"dds":0.08754208754208759,"last_synced_commit":"9b327f5022539a0e62065142a71f4dca8fc89d88"},"previous_names":[],"tags_count":63,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/balvig%2Fspyke","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/balvig%2Fspyke/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/balvig%2Fspyke/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/balvig%2Fspyke/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/balvig","download_url":"https://codeload.github.com/balvig/spyke/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248824654,"owners_count":21167344,"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":["activerecord","api","faraday","json","rest","rest-api","ruby"],"created_at":"2024-07-31T17:01:06.494Z","updated_at":"2025-04-14T04:57:17.205Z","avatar_url":"https://github.com/balvig.png","language":"Ruby","readme":"\u003cdiv align=\"center\"\u003e\n  \u003cimg src=\"spyke.svg#gh-light-mode-only\" width=\"250\" alt=\"Spyke\" /\u003e\n  \u003cimg src=\"spyke-dark.svg#gh-dark-mode-only\" width=\"250\" alt=\"Spyke\" /\u003e\n  \u003cbr/\u003e\n  Interact with remote \u003cstrong\u003eREST services\u003c/strong\u003e in an \u003cstrong\u003eActiveRecord-like\u003c/strong\u003e manner.\n  \u003cbr /\u003e\u003cbr /\u003e\n  \u003ca href=\"https://rubygems.org/gems/spyke\"\u003e\u003cimg src=\"https://badge.fury.io/rb/spyke.svg?style=flat\" alt=\"Gem Version\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://codeclimate.com/github/balvig/spyke\"\u003e\u003cimg src=\"https://codeclimate.com/github/balvig/spyke/badges/gpa.svg\" /\u003e\u003c/a\u003e\n  \u003ca href='https://coveralls.io/github/balvig/spyke'\u003e\u003cimg src=\"https://coveralls.io/repos/github/balvig/spyke/badge.svg\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://circleci.com/gh/balvig/spyke\"\u003e\u003cimg src=\"https://circleci.com/gh/balvig/spyke.svg?style=shield\" /\u003e\u003c/a\u003e\n\u003c/div\u003e\n\n---\n\nSpyke basically ~~rips off~~ takes inspiration :innocent: from [Her](https://github.com/remiprev/her), a gem which we sadly had to abandon as it gave us some performance problems and maintenance seemed to have gone stale.\n\nWe therefore made Spyke which adds a few fixes/features needed for our projects:\n\n- Fast handling of even large amounts of JSON\n- Proper support for scopes\n- Ability to define custom URIs for associations\n- ActiveRecord-like log output\n- Handling of API-side validations\n- Googlable name! :)\n\n## Configuration\n\nAdd this line to your application's Gemfile:\n\n```ruby\ngem 'spyke'\ngem 'multi_json' # or whatever is needed to parse responses\n```\n\nSpyke uses Faraday to handle requests and expects it to parse the response body into a hash in the following format:\n\n```ruby\n{ data: { id: 1, name: 'Bob' }, metadata: {}, errors: {} }\n```\n\nSo, for example for an API that returns JSON like this:\n\n```json\n{ \"result\": { \"id\": 1, \"name\": \"Bob\" }, \"extra\": {}, \"errors\": {} }\n```\n\n...the simplest possible configuration that could work is something like this:\n\n```ruby\n# config/initializers/spyke.rb\n\nclass JSONParser \u003c Faraday::Middleware\n  def on_complete(env)\n    json = MultiJson.load(env.body, symbolize_keys: true)\n    env.body = {\n      data: json[:result],\n      metadata: json[:extra],\n      errors: json[:errors]\n    }\n  end\nend\n\nSpyke::Base.connection = Faraday.new(url: 'http://api.com') do |c|\n  c.request   :multipart\n  c.request   :json # if using Faraday 1.x, please add `faraday_middleware` to your dependencies first\n  c.use       JSONParser\n  c.adapter   Faraday.default_adapter\nend\n```\n\n## Usage\n\nAdding a class and inheriting from `Spyke::Base` will allow you to interact with the remote service:\n\n```ruby\nclass User \u003c Spyke::Base\n  has_many :posts\n  scope :active, -\u003e { where(active: true) }\nend\n\nUser.all\n# =\u003e GET http://api.com/users\n\nUser.active\n# =\u003e GET http://api.com/users?active=true\n\nUser.where(age: 3).active\n# =\u003e GET http://api.com/users?active=true\u0026age=3\n\nuser = User.find(3)\n# =\u003e GET http://api.com/users/3\n\nuser.posts\n# =\u003e find embedded in returned data or GET http://api.com/users/3/posts\n\nuser.update(name: 'Alice')\n# =\u003e PUT http://api.com/users/3 - { user: { name: 'Alice' } }\n\nuser.destroy\n# =\u003e DELETE http://api.com/users/3\n\nUser.create(name: 'Bob')\n# =\u003e POST http://api.com/users - { user: { name: 'Bob' } }\n```\n\n### Custom URIs\n\nYou can specify custom URIs on both the class and association level.\nSet uri to `nil` for associations you only want to use data embedded\nin the response and never call out to the API.\n\n```ruby\nclass User \u003c Spyke::Base\n  uri 'people(/:id)' # id optional, both /people and /people/4 are valid\n\n  has_many :posts, uri: 'posts/for_user/:user_id' # user_id is required\n  has_one :image, uri: nil # only use embedded data\nend\n\nclass Post \u003c Spyke::Base\nend\n\nuser = User.find(3) # =\u003e GET http://api.com/people/3\nuser.image # Will only use embedded data and never call out to api\nuser.posts # =\u003e GET http://api.com/posts/for_user/3\nPost.find(4) # =\u003e GET http://api.com/posts/4\n```\n\n### Custom requests\n\nCustom request methods and the `with` scope methods allow you to\nperform requests for non-REST actions:\n\n```ruby\nPost.with('posts/recent') # =\u003e GET http://api.com/posts/recent\nPost.with(:recent) # =\u003e GET http://api.com/posts/recent\nPost.with(:recent).where(status: 'draft') # =\u003e GET http://api.com/posts/recent?status=draft\nPost.with(:recent).post # =\u003e POST http://api.com/posts/recent\n```\n\nCustom requests from instance:\n\n```ruby\nPost.find(3).put(:publish) # =\u003e PUT http://api.com/posts/3/publish\n```\n\nArbitrary requests (returns plain Result object):\n\n```ruby\nPost.request(:post, 'posts/3/log', time: '12:00')\n# =\u003e POST http://api.com/posts/3/log - { time: '12:00' }\n```\n\n### Custom primary keys\n\nCustom primary keys can be defined with `self.primary_key = :custom_key`:\n\n```ruby\nclass User \u003c Spyke::Base\n  self.primary_key = :user_id\n\n  # When using custom URIs the :id parameter also has to be adjusted\n  uri 'people(/:user_id)'\nend\n```\n\n### API-side validations\n\nSpyke expects errors to be formatted in the same way as the\n[ActiveModel::Errors details hash](https://cowbell-labs.com/2015-01-22-active-model-errors-details.html), ie:\n\n```ruby\n{ title: [{ error: 'blank'}, { error: 'too_short', count: 10 }] }\n```\n\nIf the API you're using returns errors in a different format you can\nremap it in Faraday to match the above. Doing this will allow you to\nshow errors returned from the server in forms and f.ex using\n`@post.errors.full_messages` just like ActiveRecord.\n\n### Error handling and fallbacks\n\nShould the API fail to connect or time out, a `Spyke::ConnectionError` will be raised.\nIf you need to recover gracefully from connection problems, you can\neither rescue that exception or use the `with_fallback` feature:\n\n```ruby\n# API is down\nArticle.all # =\u003e Spyke::ConnectionError\nArticle.with_fallback.all # =\u003e []\n\nArticle.find(1) # =\u003e Spyke::ConnectionError\nArticle.with_fallback.find(1) # =\u003e nil\n\narticle = Article.with_fallback(Article.new(title: \"Dummy\")).find(1)\narticle.title # =\u003e \"Dummy\"\n```\n\n### Attributes-wrapping\n\nSpyke, like Rails, by default wraps sent attributes in a root element,\nbut this can be disabled or customized:\n\n```ruby\nclass Article \u003c Spyke::Base\n  # Default\n  include_root_in_json true # { article: { title: ...} }\n\n  # Custom\n  include_root_in_json :post # { post: { title: ...} }\n\n  # Disabled\n  include_root_in_json false # { title: ... }\nend\n```\n\n### Using multiple APIs\n\nIf you need to use different APIs, instead of configuring `Spyke::Base`\nyou can configure each class individually:\n\n```ruby\nclass Post \u003c Spyke::Base\n  self.connection = Faraday.new(url: 'http://sashimi.com') do |faraday|\n    # middleware\n  end\nend\n```\n\n### Log output\n\nWhen used with Rails, Spyke will automatically output helpful\nActiveRecord-like messages to the main log:\n\n```bash\nStarted GET \"/posts\" for 127.0.0.1 at 2014-12-01 14:31:20 +0000\nProcessing by PostsController#index as HTML\n  Parameters: {}\n  Spyke (40.3ms)  GET http://api.com/posts [200]\nCompleted 200 OK in 75ms (Views: 64.6ms | Spyke: 40.3ms | ActiveRecord: 0ms)\n```\n\n### Other examples\n\nFor more examples of how Spyke can be used, check out [fixtures.rb](https://github.com/balvig/spyke/blob/main/test/support/fixtures.rb) and the\n[test suite](https://github.com/balvig/spyke/tree/main/test).\n","funding_links":[],"categories":["Web 后端","Clients","Ruby","HTTP","API Builder and Discovery"],"sub_categories":["Ruby Clients"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbalvig%2Fspyke","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbalvig%2Fspyke","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbalvig%2Fspyke/lists"}