purple-client 0.1.9.3 → 0.2
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 +4 -4
- data/AGENTS.md +154 -0
- data/README.md +62 -1
- data/docs/openapi_mapping.md +94 -0
- data/docs/testing_wrappers.md +80 -0
- data/examples/real_world/exchangerate_host/README.md +51 -0
- data/examples/real_world/exchangerate_host/client.rb +138 -0
- data/lib/purple/client.rb +13 -0
- data/lib/purple/version.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5ec58b7f5f8e255925536cf2a25eb351f3d1750b90c6fbb14afc1da0adff46ef
|
|
4
|
+
data.tar.gz: 4d5b8472eb6aeaccc49c9271f3bd512abeb82bd6704c33c61702b46c99b09cf2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9facb7ca2fe19250fb571127f3cf9e0b5b13713ad742b11ff10e423b6baba16bca2faddfe72f89811fbbff428ccfa7cc6fbc57f55ad08e4bca643aac7add6a32
|
|
7
|
+
data.tar.gz: a5d8da246488a4d47982f58703dd37545b722a8b638d9fcdd31e4372e127f9688f1ae77428b4b8a71bc28d19bf8027cfcb250a7f65bf35e505960d01d4fc8918
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# LLM Guide for Purple::Client Wrapper Clients
|
|
2
|
+
|
|
3
|
+
This repository uses **wrapper clients** built on `Purple::Client`. A wrapper client is a thin Ruby class that declaratively describes an API using the DSL (domain, paths, params, responses, and response schemas). The DSL then generates methods that perform HTTP requests and validate/shape responses.
|
|
4
|
+
|
|
5
|
+
**A wrapper client is NOT:**
|
|
6
|
+
- A generic HTTP client or SDK with its own request/response plumbing.
|
|
7
|
+
- A place for business logic, caching, retries, or complex data transformations.
|
|
8
|
+
- A replacement for application-level services or models.
|
|
9
|
+
|
|
10
|
+
## Golden path structure
|
|
11
|
+
|
|
12
|
+
When adding a new wrapper, follow this structure:
|
|
13
|
+
|
|
14
|
+
1. **Namespace + client class** (module name matches provider/service name)
|
|
15
|
+
2. **`domain`** declaration
|
|
16
|
+
3. **`path` tree** that mirrors API routes
|
|
17
|
+
4. **`params`** and request **`body`** definitions where applicable
|
|
18
|
+
5. **`response`** blocks for expected status codes
|
|
19
|
+
6. **`root_method`** for each callable endpoint
|
|
20
|
+
|
|
21
|
+
## Naming conventions
|
|
22
|
+
|
|
23
|
+
- `root_method` should be a **verb/action** (`convert`, `live_rates`, `historical_rates`).
|
|
24
|
+
- Parameter names should mirror the API parameter names. Ruby `snake_case` is fine—document any mapping or normalization if needed.
|
|
25
|
+
|
|
26
|
+
## Modeling guidelines
|
|
27
|
+
|
|
28
|
+
- **Query params**
|
|
29
|
+
```ruby
|
|
30
|
+
params do |base:, symbols: nil|
|
|
31
|
+
{ base: base, symbols: symbols }.compact
|
|
32
|
+
end
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- **Request bodies**
|
|
36
|
+
```ruby
|
|
37
|
+
params do |currency:, amount:|
|
|
38
|
+
{ currency: currency, amount: amount }
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
The hash returned by `params` becomes the JSON body for POST/PUT/PATCH. Use `body(...)` to describe **response** schemas.
|
|
42
|
+
|
|
43
|
+
- **Multiple responses**
|
|
44
|
+
```ruby
|
|
45
|
+
response :ok do
|
|
46
|
+
body(success: Purple::Boolean)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
response :bad_request do
|
|
50
|
+
body(error: { code: Integer, info: { type: String, allow_blank: true } })
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- **Optional / nullable fields**
|
|
55
|
+
```ruby
|
|
56
|
+
body(
|
|
57
|
+
message: { type: String, allow_blank: true },
|
|
58
|
+
provider_id: { type: String, optional: true }
|
|
59
|
+
)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- **Booleans**
|
|
63
|
+
```ruby
|
|
64
|
+
body(success: Purple::Boolean)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
- **Arrays**
|
|
68
|
+
```ruby
|
|
69
|
+
body(:array_of, id: Integer, name: String)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
- **Response transformation**
|
|
73
|
+
```ruby
|
|
74
|
+
body(result: Float) { |res| res.result }
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
## Organizing wrappers with `draw`
|
|
79
|
+
|
|
80
|
+
Use `draw` to keep large wrappers readable by splitting DSL declarations into
|
|
81
|
+
smaller files.
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
# clients/payments/client.rb
|
|
85
|
+
class Payments::Client < Purple::Client
|
|
86
|
+
domain 'https://api.example.com'
|
|
87
|
+
|
|
88
|
+
draw 'paths/invoices'
|
|
89
|
+
draw 'paths/refunds.rb'
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# clients/payments/paths/invoices.rb
|
|
93
|
+
path :invoices do
|
|
94
|
+
response :ok do
|
|
95
|
+
body :default
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
root_method :invoices
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## DO / DON'T (for LLMs)
|
|
103
|
+
|
|
104
|
+
**DO**
|
|
105
|
+
- Use the DSL (`domain`, `path`, `params`, `response`, `body`, `root_method`).
|
|
106
|
+
- Describe response schemas explicitly, even if partial.
|
|
107
|
+
- Keep the wrapper thin; add only small response transformations.
|
|
108
|
+
- Validate responses with `body(...)` so downstream callers get consistent types.
|
|
109
|
+
|
|
110
|
+
**DON'T**
|
|
111
|
+
- Invent a generic HTTP client or bypass the DSL.
|
|
112
|
+
- Add business logic, persistence, or retries inside the wrapper.
|
|
113
|
+
- Skip response validation or return raw JSON when a schema is expected.
|
|
114
|
+
|
|
115
|
+
## Skeleton wrapper template (copy/paste)
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
# frozen_string_literal: true
|
|
119
|
+
|
|
120
|
+
require "purple/client"
|
|
121
|
+
|
|
122
|
+
module ProviderName
|
|
123
|
+
class Client < Purple::Client
|
|
124
|
+
domain "https://api.provider.example"
|
|
125
|
+
|
|
126
|
+
path :resource do
|
|
127
|
+
params do |id:, verbose: nil|
|
|
128
|
+
{ id: id, verbose: verbose }.compact
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
response :ok do
|
|
132
|
+
body(
|
|
133
|
+
id: Integer,
|
|
134
|
+
name: String,
|
|
135
|
+
active: Purple::Boolean,
|
|
136
|
+
metadata: {
|
|
137
|
+
created_at: String,
|
|
138
|
+
tags: { type: Array, optional: true }
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
response :bad_request do
|
|
144
|
+
body(
|
|
145
|
+
error: String,
|
|
146
|
+
message: { type: String, allow_blank: true }
|
|
147
|
+
)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
root_method :fetch_resource
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
```
|
data/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
Purple::Client is a small DSL that helps you describe HTTP APIs. You define a domain, paths, and response structures, and the library generates handy methods for interacting with your service.
|
|
4
4
|
|
|
5
|
+
## For AI / agents
|
|
6
|
+
|
|
7
|
+
See [AGENTS.md](AGENTS.md) for an LLM-focused guide, naming conventions, and a copy-paste wrapper skeleton.
|
|
8
|
+
|
|
5
9
|
## Installation
|
|
6
10
|
|
|
7
11
|
Add the gem to your project:
|
|
@@ -347,6 +351,64 @@ end
|
|
|
347
351
|
MessagesClient.send_message
|
|
348
352
|
```
|
|
349
353
|
|
|
354
|
+
### Splitting large wrappers with `draw`
|
|
355
|
+
|
|
356
|
+
If a wrapper gets too long, you can move parts of the DSL into separate files
|
|
357
|
+
and load them with `draw`.
|
|
358
|
+
|
|
359
|
+
`draw` resolves paths relative to the file where it is called. If you omit the
|
|
360
|
+
extension, `.rb` is added automatically.
|
|
361
|
+
|
|
362
|
+
```ruby
|
|
363
|
+
# clients/payments/client.rb
|
|
364
|
+
class Payments::Client < Purple::Client
|
|
365
|
+
domain 'https://api.example.com'
|
|
366
|
+
|
|
367
|
+
draw 'paths/invoices'
|
|
368
|
+
draw 'paths/refunds.rb'
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# clients/payments/paths/invoices.rb
|
|
372
|
+
path :invoices do
|
|
373
|
+
response :ok do
|
|
374
|
+
body :default
|
|
375
|
+
end
|
|
376
|
+
root_method :invoices
|
|
377
|
+
end
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
Use `draw` only to organize DSL definitions (`path`, `params`, `response`,
|
|
381
|
+
`root_method`) into smaller files. Keep wrapper behavior declarative and avoid
|
|
382
|
+
adding non-DSL business logic in drawn files.
|
|
383
|
+
|
|
384
|
+
## How to build a wrapper client (5 steps)
|
|
385
|
+
|
|
386
|
+
1. **Define a `domain`** for the API host.
|
|
387
|
+
2. **Build the `path` tree** to reflect the API routes.
|
|
388
|
+
3. **Describe inputs** with `params` and request body fields as needed.
|
|
389
|
+
4. **Describe responses** with `response` blocks and `body` schemas.
|
|
390
|
+
5. **Set `root_method`** and add call examples in docs or specs.
|
|
391
|
+
|
|
392
|
+
Helpful references:
|
|
393
|
+
- [AGENTS.md](AGENTS.md) (LLM wrapper guide + skeleton)
|
|
394
|
+
- [OpenAPI mapping](docs/openapi_mapping.md)
|
|
395
|
+
- [Testing wrappers](docs/testing_wrappers.md)
|
|
396
|
+
- [Real-world example: exchangerate.host](examples/real_world/exchangerate_host/README.md)
|
|
397
|
+
|
|
398
|
+
## Scaffold a wrapper client
|
|
399
|
+
|
|
400
|
+
Generate a new wrapper skeleton with the built-in scaffold command:
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
bin/purple-client scaffold PROVIDER_NAME
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
Use `--force` to overwrite existing files:
|
|
407
|
+
|
|
408
|
+
```bash
|
|
409
|
+
bin/purple-client scaffold PROVIDER_NAME --force
|
|
410
|
+
```
|
|
411
|
+
|
|
350
412
|
## Development
|
|
351
413
|
|
|
352
414
|
After checking out the repo, run `bin/setup` to install dependencies. Then run
|
|
@@ -367,4 +429,3 @@ to the [code of conduct](https://github.com/[USERNAME]/purple-client/blob/main/C
|
|
|
367
429
|
|
|
368
430
|
The gem is available as open source under the terms of the
|
|
369
431
|
[MIT License](https://opensource.org/licenses/MIT).
|
|
370
|
-
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# OpenAPI → Purple::Client DSL Mapping
|
|
2
|
+
|
|
3
|
+
This guide shows how to translate common OpenAPI concepts into Purple::Client DSL constructs.
|
|
4
|
+
|
|
5
|
+
## Mapping overview
|
|
6
|
+
|
|
7
|
+
| OpenAPI concept | Purple::Client DSL |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| Path + HTTP method | `path :resource, method: :get` (nested paths reflect URL segments) |
|
|
10
|
+
| Query parameters | `params do |...| ... end` |
|
|
11
|
+
| Request body schema | Use `params` to build the request payload (for POST/PUT/PATCH, the hash returned by `params` becomes the JSON body). Use `body(...)` only for response schemas. |
|
|
12
|
+
| Response codes | `response :ok`, `response :bad_request`, `response :unprocessable_entity`, etc. |
|
|
13
|
+
| JSON object schema | `body(field: Type, nested: { ... })` |
|
|
14
|
+
| Arrays | `body(:array_of, id: Integer, name: String)` |
|
|
15
|
+
| Optional fields | `field: { type: String, optional: true }` |
|
|
16
|
+
| Nullable / blank | `field: { type: String, allow_blank: true }` |
|
|
17
|
+
| Boolean | `Purple::Boolean` |
|
|
18
|
+
|
|
19
|
+
## Worked example
|
|
20
|
+
|
|
21
|
+
### OpenAPI snippet
|
|
22
|
+
|
|
23
|
+
```yaml
|
|
24
|
+
paths:
|
|
25
|
+
/convert:
|
|
26
|
+
get:
|
|
27
|
+
parameters:
|
|
28
|
+
- in: query
|
|
29
|
+
name: from
|
|
30
|
+
schema: { type: string }
|
|
31
|
+
required: true
|
|
32
|
+
- in: query
|
|
33
|
+
name: to
|
|
34
|
+
schema: { type: string }
|
|
35
|
+
required: true
|
|
36
|
+
- in: query
|
|
37
|
+
name: amount
|
|
38
|
+
schema: { type: number }
|
|
39
|
+
required: true
|
|
40
|
+
responses:
|
|
41
|
+
"200":
|
|
42
|
+
description: Conversion result
|
|
43
|
+
content:
|
|
44
|
+
application/json:
|
|
45
|
+
schema:
|
|
46
|
+
type: object
|
|
47
|
+
properties:
|
|
48
|
+
success: { type: boolean }
|
|
49
|
+
query:
|
|
50
|
+
type: object
|
|
51
|
+
properties:
|
|
52
|
+
from: { type: string }
|
|
53
|
+
to: { type: string }
|
|
54
|
+
amount: { type: number }
|
|
55
|
+
result: { type: number }
|
|
56
|
+
"400":
|
|
57
|
+
description: Bad request
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Purple::Client DSL
|
|
61
|
+
|
|
62
|
+
```ruby
|
|
63
|
+
class ExampleClient < Purple::Client
|
|
64
|
+
domain "https://api.example.com"
|
|
65
|
+
|
|
66
|
+
path :convert do
|
|
67
|
+
params do |from:, to:, amount:|
|
|
68
|
+
{ from: from, to: to, amount: amount }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
response :ok do
|
|
72
|
+
body(
|
|
73
|
+
success: Purple::Boolean,
|
|
74
|
+
query: {
|
|
75
|
+
from: String,
|
|
76
|
+
to: String,
|
|
77
|
+
amount: Float
|
|
78
|
+
},
|
|
79
|
+
result: Float
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
response :bad_request
|
|
84
|
+
|
|
85
|
+
root_method :convert
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Notes
|
|
91
|
+
|
|
92
|
+
- Query params are produced by `params` (even for GET requests).
|
|
93
|
+
- Use `Purple::Boolean` for booleans, and `allow_blank` for fields that can be `null` or empty.
|
|
94
|
+
- For arrays, use `body(:array_of, ...)` with the element structure.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Testing Wrapper Clients with RSpec + WebMock
|
|
2
|
+
|
|
3
|
+
This guide shows how to test Purple::Client wrappers using **RSpec** and **WebMock** (no VCR).
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
Add WebMock to your development/test dependencies (already included in this repo's Gemfile):
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
# Gemfile
|
|
11
|
+
gem "webmock", "~> 3.0"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
In your spec file, require WebMock and stub HTTP calls:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
require "spec_helper"
|
|
18
|
+
require "webmock/rspec"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Example spec
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
# spec/exchangerate_host/client_spec.rb
|
|
25
|
+
require "spec_helper"
|
|
26
|
+
require "webmock/rspec"
|
|
27
|
+
require "examples/real_world/exchangerate_host/client"
|
|
28
|
+
|
|
29
|
+
RSpec.describe ExchangerateHost::Client do
|
|
30
|
+
it "calls the live rates endpoint with query params" do
|
|
31
|
+
stub_request(:get, "https://api.exchangerate.host/latest")
|
|
32
|
+
.with(query: { "base" => "USD", "symbols" => "EUR" })
|
|
33
|
+
.to_return(
|
|
34
|
+
status: 200,
|
|
35
|
+
body: {
|
|
36
|
+
success: true,
|
|
37
|
+
base: "USD",
|
|
38
|
+
date: "2024-01-01",
|
|
39
|
+
rates: { EUR: 0.92 }
|
|
40
|
+
}.to_json,
|
|
41
|
+
headers: { "Content-Type" => "application/json" }
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
response = described_class.live(base: "USD", symbols: "EUR")
|
|
45
|
+
|
|
46
|
+
expect(response.base).to eq("USD")
|
|
47
|
+
expect(response.rates[:"EUR"]).to eq(0.92)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "validates non-:ok responses" do
|
|
51
|
+
stub_request(:get, "https://api.exchangerate.host/latest")
|
|
52
|
+
.with(query: { "base" => "USD" })
|
|
53
|
+
.to_return(
|
|
54
|
+
status: 400,
|
|
55
|
+
body: {
|
|
56
|
+
success: false,
|
|
57
|
+
error: { code: 400, type: "invalid_base", info: nil }
|
|
58
|
+
}.to_json,
|
|
59
|
+
headers: { "Content-Type" => "application/json" }
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
response = described_class.live(base: "USD")
|
|
63
|
+
|
|
64
|
+
expect(response.error[:type]).to eq("invalid_base")
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## What to assert
|
|
70
|
+
|
|
71
|
+
- **HTTP method + URL + query params** via `stub_request(...).with(...)`.
|
|
72
|
+
- **Response parsing and validation** by asserting fields defined in `body(...)`.
|
|
73
|
+
- **Non-:ok responses** by stubbing other status codes (`:bad_request`, `:unprocessable_entity`).
|
|
74
|
+
- **Optional/blank fields** by stubbing missing or `null` values and ensuring no validation errors.
|
|
75
|
+
|
|
76
|
+
## Running the tests
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
bundle exec rspec
|
|
80
|
+
```
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Exchangerate Host Wrapper Example
|
|
2
|
+
|
|
3
|
+
This example shows a minimal, realistic wrapper client for [exchangerate.host](https://exchangerate.host) using `Purple::Client`.
|
|
4
|
+
|
|
5
|
+
It demonstrates:
|
|
6
|
+
- Multiple endpoints (`live`, `historical`, `convert`, `timeframe`)
|
|
7
|
+
- Required and optional params
|
|
8
|
+
- Nested response structures
|
|
9
|
+
- `optional` and `allow_blank`
|
|
10
|
+
- `Purple::Boolean`
|
|
11
|
+
- A small response transformation for `convert`
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
From the repo root:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bundle install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Run locally (optional live calls)
|
|
22
|
+
|
|
23
|
+
These examples perform live HTTP requests. If you are offline, skip them and rely on the specs.
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
ruby -r "./examples/real_world/exchangerate_host/client" -e "puts ExchangerateHost::Client.live(base: 'USD', symbols: 'EUR').rates[:eur]"
|
|
27
|
+
|
|
28
|
+
ruby -r "./examples/real_world/exchangerate_host/client" -e "puts ExchangerateHost::Client.historical('2020-01-01', base: 'USD', symbols: 'EUR').rates[:eur]"
|
|
29
|
+
|
|
30
|
+
ruby -r "./examples/real_world/exchangerate_host/client" -e "puts ExchangerateHost::Client.convert(from: 'USD', to: 'EUR', amount: 10)"
|
|
31
|
+
|
|
32
|
+
ruby -r "./examples/real_world/exchangerate_host/client" -e "puts ExchangerateHost::Client.timeframe(start_date: '2020-01-01', end_date: '2020-01-05', base: 'USD').rates.keys"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Expected response shapes
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
ExchangerateHost::Client.live(base: "USD", symbols: "EUR")
|
|
39
|
+
# => #<Purple::Responses::Object ...>
|
|
40
|
+
# response.base #=> "USD"
|
|
41
|
+
# response.rates[:"EUR"] #=> 0.92
|
|
42
|
+
|
|
43
|
+
ExchangerateHost::Client.historical("2020-01-01", base: "USD", symbols: "EUR")
|
|
44
|
+
# => #<Purple::Responses::Object ...>
|
|
45
|
+
|
|
46
|
+
ExchangerateHost::Client.convert(from: "USD", to: "EUR", amount: 10)
|
|
47
|
+
# => 9.2 (Float)
|
|
48
|
+
|
|
49
|
+
ExchangerateHost::Client.timeframe(start_date: "2020-01-01", end_date: "2020-01-05", base: "USD")
|
|
50
|
+
# => #<Purple::Responses::Object ...>
|
|
51
|
+
```
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "purple/client"
|
|
4
|
+
|
|
5
|
+
module ExchangerateHost
|
|
6
|
+
class Client < Purple::Client
|
|
7
|
+
domain "https://api.exchangerate.host"
|
|
8
|
+
|
|
9
|
+
path :latest do
|
|
10
|
+
params do |base: "USD", symbols: nil|
|
|
11
|
+
{ base: base, symbols: symbols }.compact
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
response :ok do
|
|
15
|
+
body(
|
|
16
|
+
success: Purple::Boolean,
|
|
17
|
+
base: String,
|
|
18
|
+
date: String,
|
|
19
|
+
rates: { type: Hash, allow_blank: true }
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
response :bad_request do
|
|
24
|
+
body(
|
|
25
|
+
success: Purple::Boolean,
|
|
26
|
+
error: {
|
|
27
|
+
code: Integer,
|
|
28
|
+
type: String,
|
|
29
|
+
info: { type: String, allow_blank: true }
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
root_method :live
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
path :historical do
|
|
38
|
+
path :date, is_param: true do
|
|
39
|
+
params do |base: nil, symbols: nil|
|
|
40
|
+
{ base: base, symbols: symbols }.compact
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
response :ok do
|
|
44
|
+
body(
|
|
45
|
+
success: Purple::Boolean,
|
|
46
|
+
base: String,
|
|
47
|
+
date: String,
|
|
48
|
+
rates: { type: Hash, allow_blank: true }
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
response :bad_request do
|
|
53
|
+
body(
|
|
54
|
+
success: Purple::Boolean,
|
|
55
|
+
error: {
|
|
56
|
+
code: Integer,
|
|
57
|
+
type: String,
|
|
58
|
+
info: { type: String, allow_blank: true }
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
root_method :historical
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
path :convert do
|
|
68
|
+
params do |from:, to:, amount:|
|
|
69
|
+
{ from: from, to: to, amount: amount }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
response :ok do
|
|
73
|
+
body(
|
|
74
|
+
success: Purple::Boolean,
|
|
75
|
+
query: {
|
|
76
|
+
from: String,
|
|
77
|
+
to: String,
|
|
78
|
+
amount: Float
|
|
79
|
+
},
|
|
80
|
+
info: {
|
|
81
|
+
rate: Float,
|
|
82
|
+
timestamp: { type: Integer, optional: true }
|
|
83
|
+
},
|
|
84
|
+
result: Float,
|
|
85
|
+
date: String
|
|
86
|
+
) { |res| res.result }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
response :unprocessable_entity do
|
|
90
|
+
body(
|
|
91
|
+
success: Purple::Boolean,
|
|
92
|
+
error: {
|
|
93
|
+
code: Integer,
|
|
94
|
+
type: String,
|
|
95
|
+
info: { type: String, allow_blank: true }
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
root_method :convert
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
path :timeframe do
|
|
104
|
+
params do |start_date:, end_date:, base: nil, symbols: nil|
|
|
105
|
+
{
|
|
106
|
+
start_date: start_date,
|
|
107
|
+
end_date: end_date,
|
|
108
|
+
base: base,
|
|
109
|
+
symbols: symbols
|
|
110
|
+
}.compact
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
response :ok do
|
|
114
|
+
body(
|
|
115
|
+
success: Purple::Boolean,
|
|
116
|
+
timeseries: Purple::Boolean,
|
|
117
|
+
start_date: String,
|
|
118
|
+
end_date: String,
|
|
119
|
+
base: String,
|
|
120
|
+
rates: { type: Hash, allow_blank: true }
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
response :bad_request do
|
|
125
|
+
body(
|
|
126
|
+
success: Purple::Boolean,
|
|
127
|
+
error: {
|
|
128
|
+
code: Integer,
|
|
129
|
+
type: String,
|
|
130
|
+
info: { type: String, allow_blank: true }
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
root_method :timeframe
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
data/lib/purple/client.rb
CHANGED
|
@@ -69,6 +69,19 @@ module Purple
|
|
|
69
69
|
@parent_path = path.parent
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
def draw(file_path)
|
|
73
|
+
caller_path = caller_locations(1, 1).first.absolute_path
|
|
74
|
+
base_dir = File.dirname(caller_path)
|
|
75
|
+
|
|
76
|
+
full_path = if File.extname(file_path).empty?
|
|
77
|
+
File.expand_path("#{file_path}.rb", base_dir)
|
|
78
|
+
else
|
|
79
|
+
File.expand_path(file_path, base_dir)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
class_eval(File.read(full_path), full_path)
|
|
83
|
+
end
|
|
84
|
+
|
|
72
85
|
def root_method(method_name)
|
|
73
86
|
current_path = @parent_path
|
|
74
87
|
|
data/lib/purple/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: purple-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: '0.2'
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Pavel Kalashnikov
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-02-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: dry-initializer
|
|
@@ -61,11 +61,16 @@ extra_rdoc_files: []
|
|
|
61
61
|
files:
|
|
62
62
|
- ".rspec"
|
|
63
63
|
- ".rubocop.yml"
|
|
64
|
+
- AGENTS.md
|
|
64
65
|
- CHANGELOG.md
|
|
65
66
|
- CODE_OF_CONDUCT.md
|
|
66
67
|
- LICENSE.txt
|
|
67
68
|
- README.md
|
|
68
69
|
- Rakefile
|
|
70
|
+
- docs/openapi_mapping.md
|
|
71
|
+
- docs/testing_wrappers.md
|
|
72
|
+
- examples/real_world/exchangerate_host/README.md
|
|
73
|
+
- examples/real_world/exchangerate_host/client.rb
|
|
69
74
|
- lib/purple/boolean.rb
|
|
70
75
|
- lib/purple/client.rb
|
|
71
76
|
- lib/purple/client/version.rb
|