him 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yml +40 -0
  3. data/.gitignore +6 -0
  4. data/.qlty/qlty.toml +57 -0
  5. data/.rspec +1 -0
  6. data/.ruby-version +1 -0
  7. data/.yardopts +2 -0
  8. data/CONTRIBUTING.md +26 -0
  9. data/Gemfile +2 -0
  10. data/LICENSE +8 -0
  11. data/README.md +1007 -0
  12. data/Rakefile +11 -0
  13. data/UPGRADE.md +101 -0
  14. data/gemfiles/Gemfile.activemodel-6.1 +6 -0
  15. data/gemfiles/Gemfile.activemodel-7.0 +6 -0
  16. data/gemfiles/Gemfile.activemodel-7.1 +6 -0
  17. data/gemfiles/Gemfile.activemodel-7.2 +6 -0
  18. data/gemfiles/Gemfile.activemodel-8.0 +6 -0
  19. data/him.gemspec +28 -0
  20. data/lib/him/api.rb +121 -0
  21. data/lib/him/collection.rb +21 -0
  22. data/lib/him/errors.rb +29 -0
  23. data/lib/him/json_api/model.rb +42 -0
  24. data/lib/him/middleware/accept_json.rb +18 -0
  25. data/lib/him/middleware/first_level_parse_json.rb +37 -0
  26. data/lib/him/middleware/json_api_parser.rb +65 -0
  27. data/lib/him/middleware/parse_json.rb +22 -0
  28. data/lib/him/middleware/second_level_parse_json.rb +37 -0
  29. data/lib/him/middleware.rb +12 -0
  30. data/lib/him/model/associations/association.rb +147 -0
  31. data/lib/him/model/associations/association_proxy.rb +47 -0
  32. data/lib/him/model/associations/belongs_to_association.rb +95 -0
  33. data/lib/him/model/associations/has_many_association.rb +113 -0
  34. data/lib/him/model/associations/has_one_association.rb +79 -0
  35. data/lib/him/model/associations.rb +141 -0
  36. data/lib/him/model/attributes.rb +337 -0
  37. data/lib/him/model/base.rb +33 -0
  38. data/lib/him/model/http.rb +113 -0
  39. data/lib/him/model/introspection.rb +77 -0
  40. data/lib/him/model/nested_attributes.rb +45 -0
  41. data/lib/him/model/orm.rb +306 -0
  42. data/lib/him/model/parse.rb +224 -0
  43. data/lib/him/model/paths.rb +125 -0
  44. data/lib/him/model/relation.rb +212 -0
  45. data/lib/him/model.rb +79 -0
  46. data/lib/him/version.rb +3 -0
  47. data/lib/him.rb +22 -0
  48. data/spec/api_spec.rb +120 -0
  49. data/spec/collection_spec.rb +70 -0
  50. data/spec/json_api/model_spec.rb +260 -0
  51. data/spec/middleware/accept_json_spec.rb +11 -0
  52. data/spec/middleware/first_level_parse_json_spec.rb +63 -0
  53. data/spec/middleware/json_api_parser_spec.rb +52 -0
  54. data/spec/middleware/second_level_parse_json_spec.rb +35 -0
  55. data/spec/model/associations/association_proxy_spec.rb +29 -0
  56. data/spec/model/associations_spec.rb +1010 -0
  57. data/spec/model/attributes_spec.rb +384 -0
  58. data/spec/model/callbacks_spec.rb +194 -0
  59. data/spec/model/dirty_spec.rb +133 -0
  60. data/spec/model/http_spec.rb +187 -0
  61. data/spec/model/introspection_spec.rb +110 -0
  62. data/spec/model/nested_attributes_spec.rb +135 -0
  63. data/spec/model/orm_spec.rb +717 -0
  64. data/spec/model/parse_spec.rb +619 -0
  65. data/spec/model/paths_spec.rb +348 -0
  66. data/spec/model/relation_spec.rb +255 -0
  67. data/spec/model/validations_spec.rb +45 -0
  68. data/spec/model_spec.rb +55 -0
  69. data/spec/spec_helper.rb +25 -0
  70. data/spec/support/extensions/array.rb +6 -0
  71. data/spec/support/extensions/hash.rb +6 -0
  72. data/spec/support/macros/her_macros.rb +17 -0
  73. data/spec/support/macros/model_macros.rb +36 -0
  74. data/spec/support/macros/request_macros.rb +27 -0
  75. metadata +201 -0
data/README.md ADDED
@@ -0,0 +1,1007 @@
1
+ # Him
2
+
3
+ Him is an ORM (Object Relational Mapper) that maps REST resources to Ruby objects.
4
+ It is designed to build applications that are powered by a RESTful API instead of a database.
5
+
6
+ Forked from [Her](https://github.com/remi/her) and modernized for current Ruby/Rails.
7
+
8
+ **Requirements:** Ruby >= 3.1, ActiveModel >= 6.1, Faraday >= 2.0
9
+
10
+ ---
11
+
12
+ ## Installation
13
+
14
+ In your Gemfile, add:
15
+
16
+ ```ruby
17
+ gem "him"
18
+ ```
19
+
20
+ That’s it! Him provides a `Her = Him` alias, so existing code using `Her::Model` will continue to work.
21
+
22
+ ## Usage
23
+
24
+ First, you have to define which API your models will be bound to. For example, with Rails, you would create a new `config/initializers/her.rb` file with these lines:
25
+
26
+ ```ruby
27
+ # config/initializers/her.rb
28
+ Her::API.setup url: "https://api.example.com" do |c|
29
+ # Request
30
+ c.use Faraday::Request::UrlEncoded
31
+
32
+ # Response
33
+ c.use Her::Middleware::DefaultParseJSON
34
+
35
+ # Adapter
36
+ c.adapter :net_http
37
+ end
38
+ ```
39
+
40
+ And then to add the ORM behavior to a class, you just have to include `Her::Model` in it:
41
+
42
+ ```ruby
43
+ class User
44
+ include Her::Model
45
+ end
46
+ ```
47
+
48
+ After that, using Her is very similar to many ActiveRecord-like ORMs:
49
+
50
+ ```ruby
51
+ User.all
52
+ # GET "https://api.example.com/users" and return an array of User objects
53
+
54
+ User.find(1)
55
+ # GET "https://api.example.com/users/1" and return a User object
56
+
57
+ @user = User.create(fullname: "Tobias Fünke")
58
+ # POST "https://api.example.com/users" with `fullname=Tobias+Fünke` and return the saved User object
59
+
60
+ @user = User.new(fullname: "Tobias Fünke")
61
+ @user.occupation = "actor"
62
+ @user.save
63
+ # POST "https://api.example.com/users" with `fullname=Tobias+Fünke&occupation=actor` and return the saved User object
64
+
65
+ @user = User.find(1)
66
+ @user.fullname = "Lindsay Fünke"
67
+ @user.save
68
+ # PUT "https://api.example.com/users/1" with `fullname=Lindsay+Fünke` and return the updated User object
69
+ ```
70
+
71
+ ### ActiveRecord-like methods
72
+
73
+ These are the basic ActiveRecord-like methods you can use with your models:
74
+
75
+ ```ruby
76
+ class User
77
+ include Her::Model
78
+ end
79
+
80
+ # Update a fetched resource
81
+ user = User.find(1)
82
+ user.fullname = "Lindsay Fünke" # OR user.assign_attributes(fullname: "Lindsay Fünke")
83
+ user.save # returns false if it fails, errors in user.response_errors array
84
+ # PUT "/users/1" with `fullname=Lindsay+Fünke`
85
+
86
+ # Update a resource without fetching it
87
+ User.save_existing(1, fullname: "Lindsay Fünke")
88
+ # PUT "/users/1" with `fullname=Lindsay+Fünke`
89
+
90
+ # Destroy a fetched resource
91
+ user = User.find(1)
92
+ user.destroy
93
+ # DELETE "/users/1"
94
+
95
+ # Destroy a resource without fetching it
96
+ User.destroy_existing(1)
97
+ # DELETE "/users/1"
98
+
99
+ # Fetching a collection of resources
100
+ User.all
101
+ # GET "/users"
102
+ User.where(moderator: 1).all
103
+ # GET "/users?moderator=1"
104
+
105
+ # Create a new resource
106
+ User.create(fullname: "Maeby Fünke")
107
+ # POST "/users" with `fullname=Maeby+Fünke`
108
+
109
+ # Save a new resource
110
+ user = User.new(fullname: "Maeby Fünke")
111
+ user.save! # raises Her::Errors::ResourceInvalid if it fails
112
+ # POST "/users" with `fullname=Maeby+Fünke`
113
+ ```
114
+
115
+ ## Middleware
116
+
117
+ Since Her relies on [Faraday](https://github.com/lostisland/faraday) to send HTTP requests, you can choose the middleware used to handle requests and responses. Using the block in the `setup` call, you have access to Faraday’s `connection` object and are able to customize the middleware stack used on each request and response.
118
+
119
+ ### Authentication
120
+
121
+ Her doesn’t support authentication by default. However, it’s easy to implement one with request middleware. Using the `setup` block, we can add it to the middleware stack.
122
+
123
+ For example, to add a token header to your API requests in a Rails application, you could use the excellent [`request_store`](https://rubygems.org/gems/request_store) gem like this:
124
+
125
+ ```ruby
126
+ # app/controllers/application_controller.rb
127
+ class ApplicationController < ActionController::Base
128
+ before_action :set_user_api_token
129
+
130
+ protected
131
+ def set_user_api_token
132
+ RequestStore.store[:my_api_token] = current_user.api_token # or something similar based on `session`
133
+ end
134
+ end
135
+
136
+ # lib/my_token_authentication.rb
137
+ class MyTokenAuthentication < Faraday::Middleware
138
+ def call(env)
139
+ env[:request_headers]["X-API-Token"] = RequestStore.store[:my_api_token]
140
+ @app.call(env)
141
+ end
142
+ end
143
+
144
+ # config/initializers/her.rb
145
+ require "lib/my_token_authentication"
146
+
147
+ Her::API.setup url: "https://api.example.com" do |c|
148
+ # Request
149
+ c.use MyTokenAuthentication
150
+ c.use Faraday::Request::UrlEncoded
151
+
152
+ # Response
153
+ c.use Her::Middleware::DefaultParseJSON
154
+
155
+ # Adapter
156
+ c.adapter :net_http
157
+ end
158
+ ```
159
+
160
+ Now, each HTTP request made by Her will have the `X-API-Token` header.
161
+
162
+ ### Basic HTTP Authentication
163
+
164
+ Her can use basic HTTP auth by adding a line to your initializer:
165
+
166
+ ```ruby
167
+ # config/initializers/her.rb
168
+ Her::API.setup url: "https://api.example.com" do |c|
169
+ # Request
170
+ c.request :authorization, :basic, 'myusername', 'mypassword'
171
+ c.use Faraday::Request::UrlEncoded
172
+
173
+ # Response
174
+ c.use Her::Middleware::DefaultParseJSON
175
+
176
+ # Adapter
177
+ c.adapter :net_http
178
+ end
179
+ ```
180
+
181
+ ### OAuth
182
+
183
+ Using the `faraday_middleware` and `simple_oauth` gems, it’s fairly easy to use OAuth authentication with Her.
184
+
185
+ In your Gemfile:
186
+
187
+ ```ruby
188
+ gem "her"
189
+ gem "faraday_middleware"
190
+ gem "simple_oauth"
191
+ ```
192
+
193
+ In your Ruby code:
194
+
195
+ ```ruby
196
+ # Create an application on `https://dev.twitter.com/apps` to set these values
197
+ TWITTER_CREDENTIALS = {
198
+ consumer_key: "",
199
+ consumer_secret: "",
200
+ token: "",
201
+ token_secret: ""
202
+ }
203
+
204
+ Her::API.setup url: "https://api.twitter.com/1/" do |c|
205
+ # Request
206
+ c.use FaradayMiddleware::OAuth, TWITTER_CREDENTIALS
207
+
208
+ # Response
209
+ c.use Her::Middleware::DefaultParseJSON
210
+
211
+ # Adapter
212
+ c.adapter :net_http
213
+ end
214
+
215
+ class Tweet
216
+ include Her::Model
217
+ end
218
+
219
+ @tweets = Tweet.get("/statuses/home_timeline.json")
220
+ ```
221
+
222
+ See the [*Authentication middleware section*](#authentication) for an example of how to pass different credentials based on the current user.
223
+
224
+ ### Parsing JSON data
225
+
226
+ By default, Her handles JSON data. It expects the resource/collection data to be returned at the first level.
227
+
228
+ ```javascript
229
+ // The response of GET /users/1
230
+ { "id" : 1, "name" : "Tobias Fünke" }
231
+
232
+ // The response of GET /users
233
+ [{ "id" : 1, "name" : "Tobias Fünke" }]
234
+ ```
235
+
236
+ However, if you want Her to be able to parse the data from a single root element (usually based on the model name), you’ll have to use the `parse_root_in_json` method (See the [**JSON attributes-wrapping**](#json-attributes-wrapping) section).
237
+
238
+ Also, you can define your own parsing method using a response middleware. The middleware should set `env[:body]` to a hash with three symbol keys: `:data`, `:errors` and `:metadata`. The following code uses a custom middleware to parse the JSON data:
239
+
240
+ ```ruby
241
+ # Expects responses like:
242
+ #
243
+ # {
244
+ # "result": { "id": 1, "name": "Tobias Fünke" },
245
+ # "errors": []
246
+ # }
247
+ #
248
+ class MyCustomParser < Faraday::Middleware
249
+ def on_complete(env)
250
+ json = JSON.parse(env[:body], symbolize_names: true)
251
+ env[:body] = {
252
+ data: json[:result],
253
+ errors: json[:errors],
254
+ metadata: json[:metadata]
255
+ }
256
+ end
257
+ end
258
+
259
+ Her::API.setup url: "https://api.example.com" do |c|
260
+ # Response
261
+ c.use MyCustomParser
262
+
263
+ # Adapter
264
+ c.adapter :net_http
265
+ end
266
+ ```
267
+
268
+ ### Caching
269
+
270
+ Again, using the `faraday_middleware` and `memcached` gems makes it very easy to cache requests and responses.
271
+
272
+ In your Gemfile:
273
+
274
+ ```ruby
275
+ gem "her"
276
+ gem "faraday_middleware"
277
+ gem "memcached"
278
+ ```
279
+
280
+ In your Ruby code:
281
+
282
+ ```ruby
283
+ Her::API.setup url: "https://api.example.com" do |c|
284
+ # Request
285
+ c.use FaradayMiddleware::Caching, Memcached::Rails.new('127.0.0.1:11211')
286
+
287
+ # Response
288
+ c.use Her::Middleware::DefaultParseJSON
289
+
290
+ # Adapter
291
+ c.adapter :net_http
292
+ end
293
+
294
+ class User
295
+ include Her::Model
296
+ end
297
+
298
+ @user = User.find(1)
299
+ # GET "/users/1"
300
+
301
+ @user = User.find(1)
302
+ # This request will be fetched from memcached
303
+ ```
304
+
305
+ ## Advanced Features
306
+
307
+ Here’s a list of several useful features available in Her.
308
+
309
+ ### Associations
310
+
311
+ Examples use this code:
312
+
313
+ ```ruby
314
+ class User
315
+ include Her::Model
316
+ has_many :comments
317
+ has_one :role
318
+ belongs_to :organization
319
+ end
320
+
321
+ class Comment
322
+ include Her::Model
323
+ end
324
+
325
+ class Role
326
+ include Her::Model
327
+ end
328
+
329
+ class Organization
330
+ include Her::Model
331
+ end
332
+ ```
333
+
334
+ #### Fetching data
335
+
336
+ You can define `has_many`, `has_one` and `belongs_to` associations in your models. The association data is handled in two different ways.
337
+
338
+ 1. If Her finds association data when parsing a resource, that data will be used to create the associated model objects on the resource.
339
+ 2. If no association data was included when parsing a resource, calling a method with the same name as the association will fetch the data (providing there’s an HTTP request available for it in the API).
340
+
341
+ For example, if there’s association data in the resource, no extra HTTP request is made when calling the `#comments` method and an array of resources is returned:
342
+
343
+ ```ruby
344
+ @user = User.find(1)
345
+ # GET "/users/1", response is:
346
+ # {
347
+ # "id": 1,
348
+ # "name": "George Michael Bluth",
349
+ # "comments": [
350
+ # { "id": 1, "text": "Foo" },
351
+ # { "id": 2, "text": "Bar" }
352
+ # ],
353
+ # "role": { "id": 1, "name": "Admin" },
354
+ # "organization": { "id": 2, "name": "Bluth Company" }
355
+ # }
356
+
357
+ @user.comments
358
+ # => [#<Comment id=1 text="Foo">, #<Comment id=2 text="Bar">]
359
+
360
+ @user.role
361
+ # => #<Role id=1 name="Admin">
362
+
363
+ @user.organization
364
+ # => #<Organization id=2 name="Bluth Company">
365
+ ```
366
+
367
+ If there’s no association data in the resource, Her makes a HTTP request to retrieve the data.
368
+
369
+ ```ruby
370
+ @user = User.find(1)
371
+ # GET "/users/1", response is { "id": 1, "name": "George Michael Bluth", "organization_id": 2 }
372
+
373
+ # has_many association:
374
+ @user.comments
375
+ # GET "/users/1/comments"
376
+ # => [#<Comment id=1>, #<Comment id=2>]
377
+
378
+ @user.comments.where(approved: 1)
379
+ # GET "/users/1/comments?approved=1"
380
+ # => [#<Comment id=1>]
381
+
382
+ # has_one association:
383
+ @user.role
384
+ # GET "/users/1/role"
385
+ # => #<Role id=1>
386
+
387
+ # belongs_to association:
388
+ @user.organization
389
+ # (the organization id comes from :organization_id, by default)
390
+ # GET "/organizations/2"
391
+ # => #<Organization id=2>
392
+ ```
393
+
394
+ Subsequent calls to `#comments`, `#role` and `#organization` will not trigger extra HTTP requests and will return the cached objects.
395
+
396
+ #### Creating data
397
+
398
+ You can use the association methods to build new objects and save them.
399
+
400
+ ```ruby
401
+ @user = User.find(1)
402
+ @user.comments.build(body: "Just a draft")
403
+ # => [#<Comment body="Just a draft" user_id=1>]
404
+
405
+ @user.comments.create(body: "Hello world.", user_id: 1)
406
+ # POST "/comments" with `body=Hello+world.&user_id=1`
407
+ # => [#<Comment id=3 body="Hello world." user_id=1>]
408
+ ```
409
+
410
+ You can also explicitly request a new object via the API when using ``build``. This is useful if you're dealing with default attributes.
411
+
412
+ ```ruby
413
+ class Comment
414
+ include Her::Model
415
+ request_new_object_on_build true
416
+ end
417
+
418
+ @user = User.find(1)
419
+ @user.comments.build(body: "Just a draft")
420
+ # GET "/users/1/comments/new" with `body=Just+a+draft.`
421
+ # => [#<Comment id=nil body="Just a draft" archived=false user_id=1>]
422
+ ```
423
+
424
+ #### Notes about paths
425
+
426
+ Resources must always have all the required attributes to build their complete path. For example, if you have these models:
427
+
428
+ ```ruby
429
+ class User
430
+ include Her::Model
431
+ collection_path "organizations/:organization_id/users"
432
+ end
433
+
434
+ class Organization
435
+ include Her::Model
436
+ has_many :users
437
+ end
438
+ ```
439
+
440
+ Her expects all `User` resources to have an `:organization_id` (or `:_organization_id`) attribute. Otherwise, calling mostly all methods, like `User.all`, will throw an exception like this one:
441
+
442
+ ```ruby
443
+ Her::Errors::PathError: Missing :_organization_id parameter to build the request path. Path is `organizations/:organization_id/users`. Parameters are `{ … }`.
444
+ ```
445
+
446
+ #### Associations with custom attributes
447
+
448
+ Associations can also be made using custom attributes:
449
+
450
+ ```ruby
451
+ class User
452
+ include Her::Model
453
+ belongs_to :owns, class_name: "Organization"
454
+ end
455
+
456
+ class Organization
457
+ include Her::Model
458
+ has_many :owners, class_name: "User"
459
+ end
460
+ ```
461
+
462
+ ### Validations
463
+
464
+ Her includes `ActiveModel::Validations` so you can declare validations the same way you do in Rails.
465
+
466
+ However, validations must be triggered manually — they are not run, for example, when calling `#save` on an object, or `#create` on a model class.
467
+
468
+ ```ruby
469
+ class User
470
+ include Her::Model
471
+
472
+ attributes :fullname, :email
473
+ validates :fullname, presence: true
474
+ validates :email, presence: true
475
+ end
476
+
477
+ @user = User.new(fullname: "Tobias Fünke")
478
+ @user.valid? # => false
479
+
480
+ @user.save
481
+ # POST "/users" with `fullname=Tobias+Fünke` will still be called, even if the user is not valid
482
+ ```
483
+
484
+ ### Dirty attributes
485
+
486
+ Her includes `ActiveModel::Dirty` so you can keep track of the attributes that have changed in an object.
487
+
488
+ ```ruby
489
+ class User
490
+ include Her::Model
491
+
492
+ attributes :fullname, :email
493
+ end
494
+
495
+ @user = User.new(fullname: "Tobias Fünke")
496
+ @user.fullname_changed? # => true
497
+ @user.changes # => { :fullname => [nil, "Tobias Fünke"] }
498
+
499
+ @user.save
500
+ # POST "/users" with `fullname=Tobias+Fünke`
501
+
502
+ @user.fullname_changed? # => false
503
+ @user.changes # => {}
504
+ ```
505
+
506
+ To update only the modified attributes specify `:send_only_modified_attributes => true` in the setup.
507
+
508
+ ### Callbacks
509
+
510
+ You can add *before* and *after* callbacks to your models that are triggered on specific actions. You can use symbols or blocks.
511
+
512
+ ```ruby
513
+ class User
514
+ include Her::Model
515
+ before_save :set_internal_id
516
+ after_find { |u| u.fullname.upcase! }
517
+
518
+ def set_internal_id
519
+ self.internal_id = 42 # Will be passed in the HTTP request
520
+ end
521
+ end
522
+
523
+ @user = User.create(fullname: "Tobias Fünke")
524
+ # POST "/users" with `fullname=Tobias+Fünke&internal_id=42`
525
+
526
+ @user = User.find(1)
527
+ @user.fullname # => "TOBIAS FUNKE"
528
+ ```
529
+
530
+ The available callbacks are:
531
+
532
+ * `before_save`
533
+ * `before_create`
534
+ * `before_update`
535
+ * `before_destroy`
536
+ * `after_save`
537
+ * `after_create`
538
+ * `after_update`
539
+ * `after_destroy`
540
+ * `after_find`
541
+ * `after_initialize`
542
+
543
+ ### JSON attributes-wrapping
544
+
545
+ Her supports *sending* and *parsing* JSON data wrapped in a root element (to be compatible with Rails’ `include_root_in_json` setting), like so:
546
+
547
+ #### Sending
548
+
549
+ If you want to send all data to your API wrapped in a *root* element based on the model name.
550
+
551
+ ```ruby
552
+ class User
553
+ include Her::Model
554
+ include_root_in_json true
555
+ end
556
+
557
+ class Article
558
+ include Her::Model
559
+ include_root_in_json :post
560
+ end
561
+
562
+ User.create(fullname: "Tobias Fünke")
563
+ # POST "/users" with `user[fullname]=Tobias+Fünke`
564
+
565
+ Article.create(title: "Hello world.")
566
+ # POST "/articles" with `post[title]=Hello+world`
567
+ ```
568
+
569
+ #### Parsing
570
+
571
+ If the API returns data wrapped in a *root* element based on the model name.
572
+
573
+ ```ruby
574
+ class User
575
+ include Her::Model
576
+ parse_root_in_json true
577
+ end
578
+
579
+ class Article
580
+ include Her::Model
581
+ parse_root_in_json :post
582
+ end
583
+
584
+ user = User.create(fullname: "Tobias Fünke")
585
+ # POST "/users" with `fullname=Tobias+Fünke`, response is { "user": { "fullname": "Tobias Fünke" } }
586
+ user.fullname # => "Tobias Fünke"
587
+
588
+ article = Article.create(title: "Hello world.")
589
+ # POST "/articles" with `title=Hello+world.`, response is { "post": { "title": "Hello world." } }
590
+ article.title # => "Hello world."
591
+ ```
592
+
593
+ Of course, you can use both `include_root_in_json` and `parse_root_in_json` at the same time.
594
+
595
+ #### ActiveModel::Serializers support
596
+
597
+ If the API returns data in the default format used by the
598
+ [ActiveModel::Serializers](https://github.com/rails-api/active_model_serializers)
599
+ project you need to configure Her as follows:
600
+
601
+ ```ruby
602
+ class User
603
+ include Her::Model
604
+ parse_root_in_json true, format: :active_model_serializers
605
+ end
606
+
607
+ user = Users.find(1)
608
+ # GET "/users/1", response is { "user": { "id": 1, "fullname": "Lindsay Fünke" } }
609
+
610
+ users = Users.all
611
+ # GET "/users", response is { "users": [{ "id": 1, "fullname": "Lindsay Fünke" }, { "id": 1, "fullname": "Tobias Fünke" }] }
612
+ ```
613
+
614
+ #### JSON API support
615
+
616
+ To consume a JSON API 1.0 compliant service, it must return data in accordance with the [JSON API spec](http://jsonapi.org/). The general format
617
+ of the data is as follows:
618
+
619
+ ```json
620
+ { "data": {
621
+ "type": "developers",
622
+ "id": "6ab79c8c-ec5a-4426-ad38-8763bbede5a7",
623
+ "attributes": {
624
+ "language": "ruby",
625
+ "name": "avdi grimm",
626
+ }
627
+ }
628
+ ```
629
+
630
+ Then to setup your models:
631
+
632
+ ```ruby
633
+ class Contributor
634
+ include Her::JsonApi::Model
635
+
636
+ # defaults to demodulized, pluralized class name, e.g. contributors
637
+ type :developers
638
+ end
639
+ ```
640
+
641
+ Finally, you'll need to use the included JsonApiParser Her middleware:
642
+
643
+ ```ruby
644
+ Her::API.setup url: 'https://my_awesome_json_api_service' do |c|
645
+ # Request
646
+ c.use FaradayMiddleware::EncodeJson
647
+
648
+ # Response
649
+ c.use Her::Middleware::JsonApiParser
650
+
651
+ # Adapter
652
+ c.adapter :net_http
653
+ end
654
+ ```
655
+
656
+ ### Custom requests
657
+
658
+ You can easily define custom requests for your models using `custom_get`, `custom_post`, etc.
659
+
660
+ ```ruby
661
+ class User
662
+ include Her::Model
663
+
664
+ custom_get :popular, :unpopular
665
+ custom_post :from_default, :activate
666
+ end
667
+
668
+ User.popular
669
+ # GET "/users/popular"
670
+ # => [#<User id=1>, #<User id=2>]
671
+
672
+ User.unpopular
673
+ # GET "/users/unpopular"
674
+ # => [#<User id=3>, #<User id=4>]
675
+
676
+ User.from_default(name: "Maeby Fünke")
677
+ # POST "/users/from_default" with `name=Maeby+Fünke`
678
+ # => #<User id=5 name="Maeby Fünke">
679
+
680
+ User.activate(id: 6)
681
+ # POST "/users/6/activate"
682
+ # => #<User id=6>
683
+ ```
684
+
685
+ You can also use `get`, `post`, `put` or `delete` (which maps the returned data to either a collection or a resource).
686
+
687
+ ```ruby
688
+ class User
689
+ include Her::Model
690
+ end
691
+
692
+ User.get(:popular)
693
+ # GET "/users/popular"
694
+ # => [#<User id=1>, #<User id=2>]
695
+
696
+ User.get(:single_best)
697
+ # GET "/users/single_best"
698
+ # => #<User id=1>
699
+ ```
700
+
701
+ You can also use `get_raw` which yields the parsed data and the raw response from the HTTP request. Other HTTP methods are supported (`post_raw`, `put_raw`, etc.).
702
+
703
+ ```ruby
704
+ class User
705
+ include Her::Model
706
+
707
+ def self.total
708
+ get_raw(:stats) do |parsed_data, response|
709
+ parsed_data[:data][:total_users]
710
+ end
711
+ end
712
+ end
713
+
714
+ User.total
715
+ # GET "/users/stats"
716
+ # => 42
717
+ ```
718
+
719
+ You can also use full request paths (with strings instead of symbols).
720
+
721
+ ```ruby
722
+ class User
723
+ include Her::Model
724
+ end
725
+
726
+ User.get("/users/popular")
727
+ # GET "/users/popular"
728
+ # => [#<User id=1>, #<User id=2>]
729
+ ```
730
+
731
+ ### Custom paths
732
+
733
+ You can define custom HTTP paths for your models:
734
+
735
+ ```ruby
736
+ class User
737
+ include Her::Model
738
+ collection_path "/hello_users/:id"
739
+ end
740
+
741
+ @user = User.find(1)
742
+ # GET "/hello_users/1"
743
+ ```
744
+
745
+ You can also include custom variables in your paths:
746
+
747
+ ```ruby
748
+ class User
749
+ include Her::Model
750
+ collection_path "/organizations/:organization_id/users"
751
+ end
752
+
753
+ @user = User.find(1, _organization_id: 2)
754
+ # GET "/organizations/2/users/1"
755
+
756
+ @user = User.all(_organization_id: 2)
757
+ # GET "/organizations/2/users"
758
+
759
+ @user = User.new(fullname: "Tobias Fünke", organization_id: 2)
760
+ @user.save
761
+ # POST "/organizations/2/users" with `fullname=Tobias+Fünke`
762
+ ```
763
+
764
+ ### Custom primary keys
765
+
766
+ If your record uses an attribute other than `:id` to identify itself, specify it using the `primary_key` method:
767
+
768
+ ```ruby
769
+ class User
770
+ include Her::Model
771
+ primary_key :_id
772
+ end
773
+
774
+ user = User.find("4fd89a42ff204b03a905c535")
775
+ # GET "/users/4fd89a42ff204b03a905c535", response is { "_id": "4fd89a42ff204b03a905c535", "name": "Tobias" }
776
+
777
+ user.destroy
778
+ # DELETE "/users/4fd89a42ff204b03a905c535"
779
+ ```
780
+
781
+ ### Inheritance
782
+
783
+ If all your models share the same settings, you might want to make them children of a class and only include `Her::Model` in that class. However, there are a few settings that don’t get passed to the children classes:
784
+
785
+ * `root_element`
786
+ * `collection_path` and `resource_path`
787
+
788
+ Those settings are based on the class name, so you don’t have to redefine them each time you create a new children class (but you still can). Every other setting is inherited from the parent (associations, scopes, JSON settings, etc.).
789
+
790
+ ```ruby
791
+ module MyAPI
792
+ class Model
793
+ include Her::Model
794
+
795
+ parse_root_in_json true
796
+ include_root_in_json true
797
+ end
798
+ end
799
+
800
+ class User < MyAPI::Model
801
+ end
802
+
803
+ User.find(1)
804
+ # GET "/users/1"
805
+ ```
806
+
807
+ ### Scopes
808
+
809
+ Just like with ActiveRecord, you can define named scopes for your models. Scopes are chainable and can be used within other scopes.
810
+
811
+ ```ruby
812
+ class User
813
+ include Her::Model
814
+
815
+ scope :by_role, ->(role) { where(role: role) }
816
+ scope :admins, -> { by_role('admin') }
817
+ scope :active, -> { where(active: 1) }
818
+ end
819
+
820
+ @admins = User.admins
821
+ # GET "/users?role=admin"
822
+
823
+ @moderators = User.by_role('moderator')
824
+ # GET "/users?role=moderator"
825
+
826
+ @active_admins = User.active.admins # @admins.active would have worked here too
827
+ # GET "/users?role=admin&active=1"
828
+ ```
829
+
830
+ A neat trick you can do with scopes is interact with complex paths.
831
+
832
+ ```ruby
833
+ class User
834
+ include Her::Model
835
+
836
+ collection_path "organizations/:organization_id/users"
837
+ scope :for_organization, ->(id) { where(organization_id: id) }
838
+ end
839
+
840
+ @user = User.for_organization(3).find(2)
841
+ # GET "/organizations/3/users/2"
842
+
843
+ @user = User.for_organization(3).create(fullname: "Tobias Fünke")
844
+ # POST "/organizations/3" with `fullname=Tobias+Fünke`
845
+ ```
846
+
847
+ ### Multiple APIs
848
+
849
+ It is possible to use different APIs for different models. Instead of calling `Her::API.setup`, you can create instances of `Her::API`:
850
+
851
+ ```ruby
852
+ # config/initializers/her.rb
853
+ MY_API = Her::API.new
854
+ MY_API.setup url: "https://my-api.example.com" do |c|
855
+ # Response
856
+ c.use Her::Middleware::DefaultParseJSON
857
+
858
+ # Adapter
859
+ c.adapter :net_http
860
+ end
861
+
862
+ OTHER_API = Her::API.new
863
+ OTHER_API.setup url: "https://other-api.example.com" do |c|
864
+ # Response
865
+ c.use Her::Middleware::DefaultParseJSON
866
+
867
+ # Adapter
868
+ c.adapter :net_http
869
+ end
870
+ ```
871
+
872
+ You can then define which API a model will use:
873
+
874
+ ```ruby
875
+ class User
876
+ include Her::Model
877
+ use_api MY_API
878
+ end
879
+
880
+ class Category
881
+ include Her::Model
882
+ use_api OTHER_API
883
+ end
884
+
885
+ User.all
886
+ # GET "https://my-api.example.com/users"
887
+
888
+ Category.all
889
+ # GET "https://other-api.example.com/categories"
890
+ ```
891
+
892
+ ### SSL
893
+
894
+ When initializing `Her::API`, you can pass any parameter supported by `Faraday.new`. So [to use HTTPS](https://github.com/lostisland/faraday/wiki/Setting-up-SSL-certificates), you can use Faraday’s `:ssl` option.
895
+
896
+ ```ruby
897
+ ssl_options = { ca_path: "/usr/lib/ssl/certs" }
898
+ Her::API.setup url: "https://api.example.com", ssl: ssl_options do |c|
899
+ # Response
900
+ c.use Her::Middleware::DefaultParseJSON
901
+
902
+ # Adapter
903
+ c.adapter :net_http
904
+ end
905
+ ```
906
+
907
+ ## Testing
908
+
909
+ Suppose we have these two models bound to your API:
910
+
911
+ ```ruby
912
+ # app/models/user.rb
913
+ class User
914
+ include Her::Model
915
+ custom_get :popular
916
+ end
917
+
918
+ # app/models/post.rb
919
+ class Post
920
+ include Her::Model
921
+ custom_get :recent, :archived
922
+ end
923
+ ```
924
+
925
+ In order to test them, we’ll have to stub the remote API requests. With [RSpec](https://github.com/rspec/rspec-core), we can do this like so:
926
+
927
+ ```ruby
928
+ # spec/spec_helper.rb
929
+ RSpec.configure do |config|
930
+ config.include(Module.new do
931
+ def stub_api_for(klass)
932
+ klass.use_api (api = Her::API.new)
933
+
934
+ # Here, you would customize this for your own API (URL, middleware, etc)
935
+ # like you have done in your application’s initializer
936
+ api.setup url: "http://api.example.com" do |c|
937
+ c.use Her::Middleware::FirstLevelParseJSON
938
+ c.adapter(:test) { |s| yield(s) }
939
+ end
940
+ end
941
+ end)
942
+ end
943
+ ```
944
+
945
+ Then, in your tests, we can specify what (fake) HTTP requests will return:
946
+
947
+ ```ruby
948
+ # spec/models/user.rb
949
+ describe User do
950
+ before do
951
+ stub_api_for(User) do |stub|
952
+ stub.get("/users/popular") { |env| [200, {}, [{ id: 1, name: "Tobias Fünke" }, { id: 2, name: "Lindsay Fünke" }].to_json] }
953
+ end
954
+ end
955
+
956
+ describe :popular do
957
+ subject { User.popular }
958
+ its(:length) { should == 2 }
959
+ its(:errors) { should be_empty }
960
+ end
961
+ end
962
+ ```
963
+
964
+ We can redefine the API for a model as many times as we want, like for more complex tests:
965
+
966
+ ```ruby
967
+ # spec/models/user.rb
968
+ describe Post do
969
+ describe :recent do
970
+ before do
971
+ stub_api_for(Post) do |stub|
972
+ stub.get("/posts/recent") { |env| [200, {}, [{ id: 1 }, { id: 2 }].to_json] }
973
+ end
974
+ end
975
+
976
+ subject { Post.recent }
977
+ its(:length) { should == 2 }
978
+ its(:errors) { should be_empty }
979
+ end
980
+
981
+ describe :archived do
982
+ before do
983
+ stub_api_for(Post) do |stub|
984
+ stub.get("/posts/archived") { |env| [200, {}, [{ id: 1 }, { id: 2 }].to_json] }
985
+ end
986
+ end
987
+
988
+ subject { Post.archived }
989
+ its(:length) { should == 2 }
990
+ its(:errors) { should be_empty }
991
+ end
992
+ end
993
+ ```
994
+
995
+ ## Upgrade
996
+
997
+ See the [UPGRADE.md](https://github.com/remiprev/her/blob/master/UPGRADE.md) for backward compatibility issues.
998
+
999
+ ## Contribute
1000
+
1001
+ Feel free to contribute and submit issues/pull requests [on GitHub](https://github.com/remiprev/her/issues).
1002
+
1003
+ See [CONTRIBUTING.md](https://github.com/remiprev/her/blob/master/CONTRIBUTING.md) for best practices.
1004
+
1005
+ ## License
1006
+
1007
+ Him is © 2026 [Dale Stevens](https://github.com/voltechs), based on Her © 2012-2021 [Rémi Prévost](http://exomel.com). Freely distributed under the [MIT license](https://github.com/TwilightCoders/him/blob/main/LICENSE).