webfunction 1.0.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 20a2d512457397579e72ac5b87ade364333ebd28fb2da453f9a50cc4fb11318f
4
+ data.tar.gz: 53deaf1574b81239251933221f8f1153682e6c95fe51cd0553fac1349e4be376
5
+ SHA512:
6
+ metadata.gz: 6b2ca218fc3d8500a3c4cf81c88080549b8e37776b2a7fc231e53be70ca31a7c0081fd89485cafc7226887a26ad4fc0206c6203fb4ab98a7d62fe28f0feb60b1
7
+ data.tar.gz: da4255e1d7cde67079bbd05fc7bd76a7ce42bc36daf471600cf9e8a0d917b852af0373ba890f1ab0721c5c33b1e7d77b8f6bb82641cdfab1d670b3ae8235ca84
data/.rubocop.yml ADDED
@@ -0,0 +1,42 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ SuggestExtensions: false
4
+
5
+ Metrics:
6
+ Enabled: false
7
+
8
+ Style/Documentation:
9
+ Enabled: false
10
+
11
+ Style/StringLiterals:
12
+ EnforcedStyle: double_quotes
13
+
14
+ Style/StringLiteralsInInterpolation:
15
+ EnforcedStyle: double_quotes
16
+
17
+ Style/TrailingCommaInArguments:
18
+ EnforcedStyleForMultiline: consistent_comma
19
+
20
+ Style/TrailingCommaInArrayLiteral:
21
+ EnforcedStyleForMultiline: consistent_comma
22
+
23
+ Style/TrailingCommaInHashLiteral:
24
+ EnforcedStyleForMultiline: consistent_comma
25
+
26
+ Style/RaiseArgs:
27
+ EnforcedStyle: compact
28
+
29
+ Layout/MultilineMethodCallBraceLayout:
30
+ EnforcedStyle: new_line
31
+
32
+ Style/IfUnlessModifier:
33
+ Enabled: false
34
+
35
+ Lint/SymbolConversion:
36
+ Enabled: false
37
+
38
+ Layout/ArgumentAlignment:
39
+ EnforcedStyle: with_fixed_indentation
40
+
41
+ Style/ConditionalAssignment:
42
+ EnforcedStyle: assign_inside_condition
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-02-16
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Robin Clart
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,598 @@
1
+ # webfunction
2
+
3
+ A [Web Function](https://webfunction.org) client for Ruby.
4
+
5
+ Web Function is a way to design APIs. There are no verbs and no nested URLs. You
6
+ call an endpoint with a POST request, the path names the action, and the JSON
7
+ body carries the data. This gem lets you call those endpoints from Ruby.
8
+
9
+ ```ruby
10
+ client = WebFunction::Client.from_package_endpoint("https://api.example.com/package")
11
+
12
+ client.find_user(id: "123")
13
+ # => { "id" => "123", "name" => "Ada" }
14
+ ```
15
+
16
+ ## Table of contents
17
+
18
+ - [Why Web Function](#why-web-function)
19
+ - [Installation](#installation)
20
+ - [Quick start](#quick-start)
21
+ - [Clients](#clients)
22
+ - [Calling endpoints](#calling-endpoints)
23
+ - [Pagination](#pagination)
24
+ - [Authentication](#authentication)
25
+ - [Versioning](#versioning)
26
+ - [Inspecting a package](#inspecting-a-package)
27
+ - [Types](#types)
28
+ - [Object schemas](#object-schemas)
29
+ - [Error handling](#error-handling)
30
+ - [Pipelining](#pipelining)
31
+ - [Custom HTTP client](#custom-http-client)
32
+ - [Low-level requests](#low-level-requests)
33
+ - [Command line tool](#command-line-tool)
34
+ - [Development](#development)
35
+ - [Contributing](#contributing)
36
+ - [License](#license)
37
+
38
+ ## Why Web Function
39
+
40
+ A Web Function API has a few simple rules:
41
+
42
+ - Every call is an HTTP POST.
43
+ - The request body is a JSON object.
44
+ - The response body is any JSON value.
45
+ - A `200` status means success and the body is the return value.
46
+ - A `400` status means the request was bad and the body explains why.
47
+ - Any other status is an error you handle yourself.
48
+
49
+ On top of that, an API can publish a **package**. A package is a JSON document
50
+ that lists the endpoints, their arguments, their return types, and their docs.
51
+ This gem reads a package and gives you a client that calls those endpoints as if
52
+ they were Ruby methods.
53
+
54
+ You can read the full specification at [webfunction.org](https://webfunction.org).
55
+
56
+ ## Installation
57
+
58
+ Add the gem to your project:
59
+
60
+ ```bash
61
+ bundle add webfunction
62
+ ```
63
+
64
+ Or install it on its own:
65
+
66
+ ```bash
67
+ gem install webfunction
68
+ ```
69
+
70
+ The gem needs Ruby 3.1 or newer.
71
+
72
+ ## Quick start
73
+
74
+ Point a client at a package URL and start calling endpoints:
75
+
76
+ ```ruby
77
+ require "webfunction"
78
+
79
+ client = WebFunction::Client.from_package_endpoint("https://api.example.com/package")
80
+
81
+ # An endpoint named "list-items" becomes the method "list_items".
82
+ items = client.list_items(limit: 10)
83
+ # => [{ "id" => 1 }, { "id" => 2 }]
84
+
85
+ # Pass a bearer token for endpoints that need authentication.
86
+ secure = WebFunction::Client.from_package_endpoint(
87
+ "https://api.example.com/package",
88
+ bearer_auth: "my-token",
89
+ )
90
+
91
+ secure.create_item(name: "Notebook")
92
+ # => { "id" => 3, "name" => "Notebook" }
93
+ ```
94
+
95
+ ## Clients
96
+
97
+ A `WebFunction::Client` wraps a package and turns each endpoint into a method.
98
+
99
+ You usually build a client from a package URL. The gem fetches the package,
100
+ reads its endpoints, and returns a ready client:
101
+
102
+ ```ruby
103
+ client = WebFunction::Client.from_package_endpoint("https://api.example.com/package")
104
+ ```
105
+
106
+ `from_package_endpoint` fetches the package by calling the URL as a Web
107
+ Function endpoint, that is, with a POST request. If your package document is
108
+ served as plain JSON over a regular `GET` request instead, use `from_url`:
109
+
110
+ ```ruby
111
+ client = WebFunction::Client.from_url("https://api.example.com/package.json")
112
+ ```
113
+
114
+ `from_url` accepts the same options as `from_package_endpoint`. When you pass a
115
+ `version`, it is added to the request as an `api_version` query parameter rather
116
+ than an `Api-Version` header.
117
+
118
+ If you already have a package in memory, build the client from that instead.
119
+ This avoids the extra request:
120
+
121
+ ```ruby
122
+ package = WebFunction::Package.from_hash(
123
+ "base_url" => "https://api.example.com/",
124
+ "endpoints" => [
125
+ { "name" => "list-items", "returns" => [["object"]] },
126
+ { "name" => "create-item", "returns" => ["object"] },
127
+ ],
128
+ )
129
+
130
+ client = WebFunction::Client.from_package(package)
131
+ ```
132
+
133
+ All three builders accept the same options:
134
+
135
+ | Option | Description |
136
+ | --- | --- |
137
+ | `bearer_auth` | A bearer token sent with every call. |
138
+ | `version` | A version string sent in the `Api-Version` header. |
139
+ | `pipelined` | When `true`, calls are batched into one request. See [Pipelining](#pipelining). |
140
+
141
+ You can also overwrite these using the `Client#bearer_auth=`, `Client#version=`
142
+ and `Client#pipeline=` attribute writers after having instantiated a client.
143
+
144
+ ## Calling endpoints
145
+
146
+ Endpoint names use dashes, like `list-items`. The client exposes them as Ruby
147
+ methods with underscores, like `list_items`. You pass arguments as keywords:
148
+
149
+ ```ruby
150
+ client.list_items(limit: 10, offset: 20)
151
+ ```
152
+
153
+ The return value is the parsed JSON response. It can be a hash, an array, a
154
+ string, a number, a boolean, or `nil`. When the response matches the pagination
155
+ contract, it is wrapped in a `WebFunction::Page` instead — see
156
+ [Pagination](#pagination).
157
+
158
+ ```ruby
159
+ client.get_count # => 42
160
+ client.list_items # => [{ "id" => 1 }]
161
+ client.find_user(id: "1") # => { "id" => "1", "name" => "Ada" }
162
+ ```
163
+
164
+ If you prefer to call an endpoint by its name, use `call`:
165
+
166
+ ```ruby
167
+ client.call("list-items", limit: 10)
168
+ ```
169
+
170
+ Calling an endpoint that the package does not define raises `NoMethodError`:
171
+
172
+ ```ruby
173
+ client.does_not_exist
174
+ # => NoMethodError
175
+ ```
176
+
177
+ ## Pagination
178
+
179
+ Some endpoints return results in pages. A paginated response is a JSON object
180
+ with three keys: `page` (the items), `next`, and `previous`. The gem detects
181
+ this shape automatically and returns a `WebFunction::Page` instead of a bare
182
+ hash.
183
+
184
+ ```ruby
185
+ page = client.list_people(filters: { first_name: "Joe" })
186
+ # => #<WebFunction::Page>
187
+
188
+ page.page
189
+ # => [{ "person_id" => "person_1", "first_name" => "Joe" }, ...]
190
+
191
+ page.next? # => true
192
+ page.previous? # => false
193
+ ```
194
+
195
+ `Page` is Enumerable over the current page's items:
196
+
197
+ ```ruby
198
+ page.map { |person| person["person_id"] }
199
+ ```
200
+
201
+ To move between pages, call `next_page` or `previous_page`. Each call posts the
202
+ opaque `next` or `previous` body from the last response to the same endpoint —
203
+ you never build or change those bodies yourself:
204
+
205
+ ```ruby
206
+ next_page = page.next_page
207
+ # => #<WebFunction::Page>
208
+
209
+ next_page.previous?
210
+ # => true
211
+
212
+ next_page.previous_page
213
+ # => back to the earlier page
214
+ ```
215
+
216
+ When there is no adjacent page, `next?` / `previous?` are `false` and
217
+ `next_page` / `previous_page` return `nil`.
218
+
219
+ You can ask an endpoint whether it declares the `paginated` flag:
220
+
221
+ ```ruby
222
+ endpoint = client.package.endpoint("list-people")
223
+ endpoint.paginated? # => true
224
+ ```
225
+
226
+ The full contract is at [webfunction.org/pagination](https://webfunction.org/pagination).
227
+
228
+ ## Authentication
229
+
230
+ Some endpoints need a bearer token. Pass it when you build the client and the
231
+ gem adds an `Authorization: Bearer <token>` header to every call:
232
+
233
+ ```ruby
234
+ client = WebFunction::Client.from_package_endpoint(
235
+ "https://api.example.com/package",
236
+ bearer_auth: "my-token",
237
+ )
238
+
239
+ client.list_orders
240
+ ```
241
+
242
+ The gem does not handle login. How you obtain the token is up to you. To find
243
+ out whether an endpoint needs a token, check its `bearer_auth?` flag:
244
+
245
+ ```ruby
246
+ endpoint = client.package.endpoint("list-orders")
247
+ endpoint.bearer_auth? # => true
248
+ endpoint.capture_bearer? # => false
249
+ ```
250
+
251
+ ## Versioning
252
+
253
+ A versioned package selects its version through the `Api-Version` header. Pass a
254
+ version string when you build the client:
255
+
256
+ ```ruby
257
+ client = WebFunction::Client.from_package_endpoint(
258
+ "https://api.example.com/package",
259
+ version: "2024-01-01",
260
+ )
261
+ ```
262
+
263
+ You can ask a package whether it is versioned and which versions it offers:
264
+
265
+ ```ruby
266
+ package = client.package
267
+ package.versioned? # => true
268
+ package.version # => "2024-01-01"
269
+ package.versions # => ["2023-06-01", "2024-01-01"]
270
+ ```
271
+
272
+ ## Inspecting a package
273
+
274
+ A package describes itself. You can read its metadata, walk its endpoints, and
275
+ look at the arguments and outputs of each one. This is useful for building docs
276
+ or for checking a call before you make it.
277
+
278
+ ```ruby
279
+ package = client.package
280
+
281
+ package.name # => "Example API"
282
+ package.base_url # => "https://api.example.com/"
283
+ package.docs # => "Markdown documentation for the package."
284
+ package.endpoints # => [#<WebFunction::Endpoint ...>, ...]
285
+ ```
286
+
287
+ Look up a single endpoint by name. Underscores and dashes both work:
288
+
289
+ ```ruby
290
+ endpoint = package.endpoint("find-user")
291
+ # Same as:
292
+ endpoint = package.endpoint(:find_user)
293
+
294
+ endpoint.name # => "find-user"
295
+ endpoint.docs # => "Retrieves user data."
296
+ endpoint.returns # => a Type, see below
297
+ endpoint.returns.to_s # => "object"
298
+ endpoint.group # => "Users"
299
+ ```
300
+
301
+ Each endpoint lists the arguments it takes:
302
+
303
+ ```ruby
304
+ endpoint.arguments
305
+ # => [#<WebFunction::Argument ...>]
306
+
307
+ id = endpoint.argument("id")
308
+ id.name # => "id"
309
+ id.type # => a Type, see below
310
+ id.type.to_s # => "string"
311
+ id.required? # => true
312
+ id.optional? # => false
313
+ id.choices # => []
314
+ id.docs # => "Identifier of the user."
315
+ ```
316
+
317
+ It also lists the attributes it returns when the return type is an object:
318
+
319
+ ```ruby
320
+ name = endpoint.attribute("name")
321
+ name.name # => "name"
322
+ name.type # => a Type, see below
323
+ name.type.to_s # => "string"
324
+ name.nullable? # => false
325
+ name.values # => []
326
+ ```
327
+
328
+ You can call an endpoint object directly once it belongs to a client:
329
+
330
+ ```ruby
331
+ endpoint = client.package.endpoint("find-user")
332
+ endpoint.call(id: "123")
333
+ # => { "id" => "123", "name" => "Ada" }
334
+ ```
335
+
336
+ ## Types
337
+
338
+ An endpoint's `returns`, an argument's `type`, and an attribute's `type` are all
339
+ `WebFunction::Type` objects rather than plain strings. A type is parsed from the
340
+ package once and gives you a richer view of what a value may look like.
341
+
342
+ Every type responds to `to_s` and `format`, so you can render it however your
343
+ docs need:
344
+
345
+ ```ruby
346
+ type = endpoint.argument("email").type
347
+
348
+ type.to_s # => "string.email"
349
+ type.format(:compact) # => "email"
350
+ type.format(:base) # => "string"
351
+ ```
352
+
353
+ A type also knows how to validate a value against itself. This checks both the
354
+ base type and any refinement, like `email` or `uuid`:
355
+
356
+ ```ruby
357
+ type.valid?("ada@example.com") # => true
358
+ type.valid?("not-an-email") # => false
359
+ ```
360
+
361
+ The base types are `string`, `number`, `object`, `boolean`, and `null`.
362
+ `string` and `number` may carry a refinement that narrows the value further:
363
+
364
+ - `string`: `date`, `time`, `datetime`, `uuid`, `base64`, `email`, `phone`,
365
+ `url`, `uri`, `ipv4`, `ipv6`, `hostname`.
366
+ - `number`: `u32`, `u64`, `i32`, `i64`, `f32`, `f64`, `timestamp`.
367
+
368
+ Types compose. A package can declare an array of a type, a union of several
369
+ types, or an open `any` type. A top-level array of type strings is read as a
370
+ union of those types, while a nested array denotes an array whose elements have
371
+ the inner type:
372
+
373
+ ```ruby
374
+ WebFunction::Type.parse(["object", "null"]).to_s # => "object | null"
375
+ WebFunction::Type.parse([["string"]]).to_s # => "array<string>"
376
+ WebFunction::Type.parse("array").to_s # => "array<any>"
377
+ WebFunction::Type.parse(nil).to_s # => "any"
378
+ ```
379
+
380
+ When a type refers to a named object definition (see below), `objects` lists the
381
+ names it references:
382
+
383
+ ```ruby
384
+ WebFunction::Type.parse("object.user").objects # => ["user"]
385
+ ```
386
+
387
+ ## Object schemas
388
+
389
+ A package can declare named object definitions under its `objects` key. Any type
390
+ can then refer to one as `object.<name>`. This lets several endpoints share the
391
+ same object shape instead of repeating its fields.
392
+
393
+ List the objects a package defines, or look one up by name:
394
+
395
+ ```ruby
396
+ package.objects
397
+ # => [#<WebFunction::ObjectSchema ...>]
398
+ ```
399
+
400
+ An object may be referenced in two different contexts, and each context uses a
401
+ different set of members:
402
+
403
+ - In an **argument** context (an argument's `type`), its `arguments` describe
404
+ its fields.
405
+ - In an **attribute** context (an endpoint's `returns` or an attribute's
406
+ `type`), its `attributes` describe its fields.
407
+
408
+ Because the same object may appear in both contexts, `object` takes a `context:`
409
+ so you get back the right member set:
410
+
411
+ ```ruby
412
+ user = package.object("user", context: :attributes)
413
+
414
+ user.name # => "user"
415
+ user.attributes # => [#<WebFunction::Attribute ...>]
416
+ user.attribute("email").type.to_s # => "string.email"
417
+ ```
418
+
419
+ If the object is not defined, or defines no members for the requested context,
420
+ `object` returns `nil`.
421
+
422
+ ## Error handling
423
+
424
+ Every error this gem raises inherits from `WebFunction::Error`. Each error
425
+ carries a `code` and optional `details`.
426
+
427
+ ```ruby
428
+ begin
429
+ client.find_user(id: "missing")
430
+ rescue WebFunction::Error => e
431
+ e.code # => "USER_NOT_FOUND"
432
+ e.message # => "No user with that id."
433
+ e.details # => { "id" => "missing" }
434
+ end
435
+ ```
436
+
437
+ These are the error classes:
438
+
439
+ | Class | Raised when |
440
+ | --- | --- |
441
+ | `WebFunction::BadRequestError` | The server replied with status `400`. |
442
+ | `WebFunction::UnexpectedStatusCodeError` | The server replied with a status other than `200` or `400`. |
443
+ | `WebFunction::JsonParseError` | The response body was not valid JSON. |
444
+ | `WebFunction::UnresolvedPromiseError` | A pipeline promise was read before it resolved. |
445
+
446
+ When the server returns a `400`, the body is an error triple. A triple is a
447
+ JSON array with three parts: a code, a message, and details. The gem reads the
448
+ triple and fills in the error:
449
+
450
+ ```json
451
+ ["USER_NOT_FOUND", "No user with that id.", { "id": "missing" }]
452
+ ```
453
+
454
+ ```ruby
455
+ rescue WebFunction::BadRequestError => e
456
+ e.code # => "USER_NOT_FOUND"
457
+ e.message # => "No user with that id."
458
+ e.details # => { "id" => "missing" }
459
+ ```
460
+
461
+ If the body is not a triple, the gem still raises `BadRequestError` with the
462
+ code `WFN_BAD_REQUEST_ERROR` and puts the raw body in `details`.
463
+
464
+ An endpoint can document the errors it may return. Read them when the endpoint
465
+ uses the `error_triple` flag:
466
+
467
+ ```ruby
468
+ endpoint.errors
469
+ # => [#<WebFunction::DocumentedError code="USER_NOT_FOUND" ...>]
470
+
471
+ error = endpoint.error("USER_NOT_FOUND")
472
+ error.code # => "USER_NOT_FOUND"
473
+ error.docs # => "Returned when no user matches the id."
474
+ ```
475
+
476
+ The package can document shared errors too:
477
+
478
+ ```ruby
479
+ package.errors
480
+ package.error("RATE_LIMITED")
481
+ ```
482
+
483
+ ## Pipelining
484
+
485
+ Pipelining sends several calls in one HTTP request. The server runs them in
486
+ order and you can feed the output of one call into the next. This cuts the
487
+ number of round trips.
488
+
489
+ Build a pipelined client and each call returns a `WebFunction::Promise` instead
490
+ of a value. A promise stands in for a result that does not exist yet:
491
+
492
+ ```ruby
493
+ client = WebFunction::Client.from_package_endpoint(
494
+ "https://api.example.com/package",
495
+ pipelined: true,
496
+ )
497
+
498
+ user = client.find_user(id: "123") # => a Promise
499
+ order = client.create_order(user_id: user["id"]) # uses the first result
500
+
501
+ order.resolve
502
+ # => { "id" => "order-1", "user_id" => "123" }
503
+ ```
504
+
505
+ Reading `user["id"]` before the call runs does not return a value. It returns a
506
+ path into the future result. The gem sends that path to the server, and the
507
+ server fills it in when it runs the second call. Calling `resolve` runs the
508
+ whole pipeline and returns the value.
509
+
510
+ Once a pipeline runs, every promise from that batch holds its value:
511
+
512
+ ```ruby
513
+ user.resolve # runs the pipeline
514
+ order.value # already available, no extra request
515
+ ```
516
+
517
+ You can also drive a pipeline by hand with `WebFunction::Pipeline`. Each step is
518
+ a hash with a `url`, `headers`, and `body`:
519
+
520
+ ```ruby
521
+ pipeline = WebFunction::Pipeline.new("https://api.example.com/run-pipeline")
522
+
523
+ pipeline.add_step(url: "https://api.example.com/a", headers: {}, body: {})
524
+ pipeline.add_step(url: "https://api.example.com/b", headers: {}, body: {})
525
+
526
+ pipeline.execute(returns: :all)
527
+ # => [{ "a" => 1 }, { "b" => 2 }]
528
+ ```
529
+
530
+ The `returns` option controls what comes back:
531
+
532
+ - `:all` returns every step result as an array. This is the default.
533
+ - `:last` returns only the last step result.
534
+ - A JSONPath string returns the value at that path, for example `"$[0].id"`.
535
+
536
+ ## Custom HTTP client
537
+
538
+ By default the gem makes requests with [Excon](https://github.com/excon/excon).
539
+ You can swap in any HTTP client by setting `WebFunction::Request.http_client` to
540
+ an object that responds to `call`.
541
+
542
+ The object receives the URL, the headers, and the JSON body. It must return a
543
+ two-element array of the status code and the raw response body:
544
+
545
+ ```ruby
546
+ WebFunction::Request.http_client = ->(url, headers, body) do
547
+ response = MyHttp.post(url, headers: headers, body: body)
548
+ [response.status, response.body]
549
+ end
550
+ ```
551
+
552
+ This is also handy in tests, where you can return a canned response without
553
+ making a real request:
554
+
555
+ ```ruby
556
+ WebFunction::Request.http_client = ->(_url, _headers, _body) do
557
+ [200, JSON.generate({ "id" => "123" })]
558
+ end
559
+ ```
560
+
561
+ ## Low-level requests
562
+
563
+ If you do not need a package, you can call a single endpoint URL directly with
564
+ `WebFunction::Request`:
565
+
566
+ ```ruby
567
+ WebFunction::Request.execute(
568
+ "https://api.example.com/find-user",
569
+ bearer_auth: "my-token",
570
+ version: "2024-01-01",
571
+ args: { id: "123" },
572
+ )
573
+ # => { "id" => "123", "name" => "Ada" }
574
+ ```
575
+
576
+ The request adds the standard headers, posts the JSON body, and parses the
577
+ response. It raises the same errors described in [Error handling](#error-handling).
578
+
579
+ ## Development
580
+
581
+ After you check out the repo, run `bin/setup` to install dependencies. Then run
582
+ `rake test` to run the tests. Run `bin/console` for a prompt where you can
583
+ experiment with the code.
584
+
585
+ To install the gem on your machine, run `bundle exec rake install`. To release a
586
+ new version, update the version in `lib/webfunction/version.rb`, then run
587
+ `bundle exec rake release`. This creates a git tag, pushes the commits and the
588
+ tag, and pushes the `.gem` file to [rubygems.org](https://rubygems.org).
589
+
590
+ ## Contributing
591
+
592
+ Bug reports and pull requests are welcome on GitHub at
593
+ [github.com/webfunction-protocol/webfunction-ruby](https://github.com/webfunction-protocol/webfunction-ruby).
594
+
595
+ ## License
596
+
597
+ This gem is open source under the terms of the
598
+ [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "minitest/test_task"
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -0,0 +1 @@
1
+ require_relative "webfunction"