prusalink 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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +170 -0
- data/lib/prusalink/auto_initializable.rb +224 -0
- data/lib/prusalink/base_resource.rb +92 -0
- data/lib/prusalink/client.rb +190 -0
- data/lib/prusalink/deserializable.rb +491 -0
- data/lib/prusalink/digest_auth.rb +80 -0
- data/lib/prusalink/exceptions.rb +36 -0
- data/lib/prusalink/files/file.rb +87 -0
- data/lib/prusalink/files/folder.rb +84 -0
- data/lib/prusalink/files/refs.rb +36 -0
- data/lib/prusalink/files.rb +184 -0
- data/lib/prusalink/info.rb +82 -0
- data/lib/prusalink/job/file.rb +70 -0
- data/lib/prusalink/job.rb +191 -0
- data/lib/prusalink/server_version.rb +70 -0
- data/lib/prusalink/status/camera.rb +26 -0
- data/lib/prusalink/status/job.rb +41 -0
- data/lib/prusalink/status/printer.rb +136 -0
- data/lib/prusalink/status/storage.rb +40 -0
- data/lib/prusalink/status/transfer.rb +40 -0
- data/lib/prusalink/status.rb +64 -0
- data/lib/prusalink/storage.rb +87 -0
- data/lib/prusalink/transfer.rb +108 -0
- data/lib/prusalink/types.rb +27 -0
- data/lib/prusalink/version.rb +7 -0
- data/lib/prusalink.rb +77 -0
- data/lib/tapioca/dsl/compilers/auto_initializable.rb +88 -0
- metadata +158 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 51c60c7c6eff2e6ac93c9bb3e3e95973f756dd33b01e5b7064667f9b3b460c9e
|
|
4
|
+
data.tar.gz: 1986ff7d2a7ef7f183e0259003181b3fc912adfc44e1aad9b0978ebbbaf90e4c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c1e9d55a44bb104802c96a1232383b13a7e589d61e2abee09349d05efd0718c6bb2711ee1f05dfc9c6888a6a2d45fbf8337204c44501821650dc6ff592e49a91
|
|
7
|
+
data.tar.gz: 7d6a4f0e004066aeb8550b7368952548e616617c33c8b265ec110ec3ed60ce761b557cf0b732bf799bd9c7f2eb2f170e52cf6b9d5212144aae4d9c8a7fae6887
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sophie Déziel
|
|
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,170 @@
|
|
|
1
|
+
# PrusaLink Ruby Gem
|
|
2
|
+
|
|
3
|
+
A type-safe Ruby wrapper for **PrusaLink**, the local REST API built into Prusa 3D printers, with full Sorbet integration.
|
|
4
|
+
|
|
5
|
+
Works with printers running PrusaLink: MK4/MK4S, MK3.9, MK3.5, XL, MINI/MINI+ and Core One with recent firmware, and MK3/MK3S/MK2.5 with a Raspberry Pi running PrusaLink.
|
|
6
|
+
|
|
7
|
+
Same architecture as the [`octoprint`](https://github.com/sophiedeziel/octoprint) gem.
|
|
8
|
+
|
|
9
|
+
## ✨ Key Features
|
|
10
|
+
|
|
11
|
+
- **🔐 Digest auth built in**: PrusaLink uses HTTP Digest authentication, and the gem handles the challenge, nonce rotation and retries for you. API keys (older firmware) work too.
|
|
12
|
+
- **🛡️ Type Safety**: Sorbet signatures on every resource, and RBI files generated for every attribute
|
|
13
|
+
- **🔥 Smart Error Handling**: HTTP statuses mapped to specific exception types
|
|
14
|
+
- **🎯 Multi-Printer Support**: thread-local clients, and `client.use { }` to talk to several printers
|
|
15
|
+
- **⚡ Modern Ruby**: Ruby 3.3+, Zeitwerk autoloading
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem "prusalink", github: "sophiedeziel/prusalink"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
### Configuration
|
|
26
|
+
|
|
27
|
+
Turn on PrusaLink on the printer (Settings > Network > PrusaLink). The screen shows the username (`maker` by default) and the password.
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require "prusalink"
|
|
31
|
+
|
|
32
|
+
Prusalink.configure(host: "http://192.168.1.42", password: "from-the-printer-screen")
|
|
33
|
+
|
|
34
|
+
# Older firmware with an API key
|
|
35
|
+
Prusalink.configure(host: "http://192.168.1.43", api_key: "abcdef")
|
|
36
|
+
|
|
37
|
+
# Custom username and timeouts (defaults: open_timeout 3s, timeout 10s)
|
|
38
|
+
Prusalink.configure(host: "http://prusa-xl.local", username: "maker", password: "...", timeout: 30)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Several printers
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
mk4 = Prusalink::Client.new(host: "http://192.168.1.42", password: "...")
|
|
45
|
+
mini = Prusalink::Client.new(host: "http://192.168.1.43", api_key: "...")
|
|
46
|
+
|
|
47
|
+
mk4.use { Prusalink::Status.get }
|
|
48
|
+
mini.use { Prusalink::Status.get }
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Printer status and telemetry
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
status = Prusalink::Status.get
|
|
55
|
+
status.printer.state # => "PRINTING"
|
|
56
|
+
status.printer.printing? # => true
|
|
57
|
+
status.printer.temp_nozzle # => 214.9
|
|
58
|
+
status.printer.temp_bed # => 59.5
|
|
59
|
+
status.job&.progress # => 42.0
|
|
60
|
+
status.job&.time_remaining # => 520 (seconds)
|
|
61
|
+
|
|
62
|
+
info = Prusalink::Info.get
|
|
63
|
+
info.name # => "MuadDib"
|
|
64
|
+
info.serial # => "CZPX4720X004XC34242"
|
|
65
|
+
|
|
66
|
+
Prusalink::ServerVersion.get.text # => "PrusaLink"
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Jobs
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
job = Prusalink::Job.get # => nil when nothing is printing
|
|
73
|
+
job.state # => "PRINTING"
|
|
74
|
+
job.progress # => 42.0
|
|
75
|
+
job.file.display_name # => "benchy_0.4n_0.2mm_PLA_MK4_1h2m.bgcode"
|
|
76
|
+
|
|
77
|
+
job.pause
|
|
78
|
+
job.resume
|
|
79
|
+
job.stop
|
|
80
|
+
|
|
81
|
+
Prusalink::Job.pause(job.id) # or by id
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Files and storage
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
Prusalink::Storage.list.map(&:path) # => ["/usb"]
|
|
88
|
+
|
|
89
|
+
# Upload, and optionally start printing
|
|
90
|
+
Prusalink::Files.upload("benchy.bgcode", storage: "usb", path: "benchy.bgcode", print_after_upload: true)
|
|
91
|
+
|
|
92
|
+
# Browse
|
|
93
|
+
folder = Prusalink::Files.get(storage: "usb")
|
|
94
|
+
folder.children.map(&:display_name)
|
|
95
|
+
|
|
96
|
+
# Print, check and delete existing files
|
|
97
|
+
Prusalink::Files.print(storage: "usb", path: "benchy.bgcode")
|
|
98
|
+
Prusalink::Files.exists?(storage: "usb", path: "benchy.bgcode") # => true
|
|
99
|
+
Prusalink::Files.delete(storage: "usb", path: "benchy.bgcode")
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
For large uploads, give the client a longer `timeout:`.
|
|
103
|
+
|
|
104
|
+
### Transfers
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
transfer = Prusalink::Transfer.get # => nil when nothing is uploading
|
|
108
|
+
transfer&.progress # => 65.82
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Error handling
|
|
112
|
+
|
|
113
|
+
```ruby
|
|
114
|
+
begin
|
|
115
|
+
Prusalink::Job.pause(42)
|
|
116
|
+
rescue Prusalink::Exceptions::ConflictError => e
|
|
117
|
+
# The printer isn't printing
|
|
118
|
+
rescue Prusalink::Exceptions::AuthenticationError
|
|
119
|
+
# Wrong password or API key
|
|
120
|
+
rescue Prusalink::Exceptions::TimeoutError, Prusalink::Exceptions::ConnectionError
|
|
121
|
+
# Printer offline or unreachable
|
|
122
|
+
end
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
| Status | Exception |
|
|
126
|
+
|---|---|
|
|
127
|
+
| 400 | `BadRequestError` |
|
|
128
|
+
| 401, 403 | `AuthenticationError` |
|
|
129
|
+
| 404 | `NotFoundError` |
|
|
130
|
+
| 409 | `ConflictError` |
|
|
131
|
+
| 415 | `UnsupportedMediaTypeError` |
|
|
132
|
+
| 500 | `InternalServerError` |
|
|
133
|
+
| 501 | `NotImplementedError` |
|
|
134
|
+
| 503 | `ServiceUnavailableError` |
|
|
135
|
+
| other | `UnknownError` |
|
|
136
|
+
|
|
137
|
+
All of them inherit from `Prusalink::Exceptions::Error`.
|
|
138
|
+
|
|
139
|
+
## API coverage
|
|
140
|
+
|
|
141
|
+
| Endpoint | Resource |
|
|
142
|
+
|---|---|
|
|
143
|
+
| `GET /api/version` | `ServerVersion.get` |
|
|
144
|
+
| `GET /api/v1/info` | `Info.get` |
|
|
145
|
+
| `GET /api/v1/status` | `Status.get` |
|
|
146
|
+
| `GET/DELETE /api/v1/job`, `PUT .../pause`, `.../resume`, `.../continue` | `Job` |
|
|
147
|
+
| `GET /api/v1/storage` | `Storage.list` |
|
|
148
|
+
| `GET/DELETE /api/v1/transfer` | `Transfer` |
|
|
149
|
+
| `GET/PUT/POST/HEAD/DELETE /api/v1/files/{storage}/{path}` | `Files` |
|
|
150
|
+
| `/api/v1/cameras/*` | not yet |
|
|
151
|
+
| `/api/v1/update/*` | not yet |
|
|
152
|
+
|
|
153
|
+
## Development
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
bin/setup # install dependencies
|
|
157
|
+
bin/check # rspec + rubocop + sorbet
|
|
158
|
+
bin/tapioca dsl # regenerate RBIs after changing auto_attr declarations
|
|
159
|
+
bin/console # interactive prompt
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
To record VCR cassettes against a real printer, see [CLAUDE.md](CLAUDE.md).
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
166
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
167
|
+
|
|
168
|
+
## Code of Conduct
|
|
169
|
+
|
|
170
|
+
Everyone interacting in this project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](CODE_OF_CONDUCT.md).
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Prusalink
|
|
5
|
+
# AutoInitializable provides automatic initialization and typed attribute generation.
|
|
6
|
+
#
|
|
7
|
+
# This module eliminates repetitive initialization code by automatically generating:
|
|
8
|
+
# - Typed instance variables from keyword arguments
|
|
9
|
+
# - Sorbet-typed attr_reader methods
|
|
10
|
+
# - Type conversion for nested objects
|
|
11
|
+
#
|
|
12
|
+
# @example Basic usage
|
|
13
|
+
# class User
|
|
14
|
+
# include AutoInitializable
|
|
15
|
+
#
|
|
16
|
+
# auto_attr :name, type: String, nilable: false
|
|
17
|
+
# auto_attr :email, type: String
|
|
18
|
+
# auto_attr :age, type: Integer
|
|
19
|
+
#
|
|
20
|
+
# auto_initialize!
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# user = User.new(name: "John", email: "john@example.com", age: 30)
|
|
24
|
+
# user.name # => "John" (typed as String)
|
|
25
|
+
# user.email # => "john@example.com" (typed as T.nilable(String))
|
|
26
|
+
#
|
|
27
|
+
# @example With custom classes and arrays
|
|
28
|
+
# class Profile
|
|
29
|
+
# include AutoInitializable
|
|
30
|
+
#
|
|
31
|
+
# auto_attr :user, type: User
|
|
32
|
+
# auto_attr :tags, type: String, array: true
|
|
33
|
+
# auto_attr :metadata, type: Hash
|
|
34
|
+
#
|
|
35
|
+
# auto_initialize!
|
|
36
|
+
# end
|
|
37
|
+
#
|
|
38
|
+
# profile = Profile.new(
|
|
39
|
+
# user: { name: "John", email: "john@example.com" }, # Auto-converted to User
|
|
40
|
+
# tags: ["admin", "developer"], # Typed as T.nilable(T::Array[String])
|
|
41
|
+
# metadata: { created_at: Time.now } # Preserved as Hash
|
|
42
|
+
# )
|
|
43
|
+
#
|
|
44
|
+
# @example Type conversion behavior
|
|
45
|
+
# # Basic Ruby types (Hash, Array, String, Integer) are preserved as-is
|
|
46
|
+
# auto_attr :data, type: Hash # No conversion, preserves original hash
|
|
47
|
+
#
|
|
48
|
+
# # Custom classes are auto-converted from hash data
|
|
49
|
+
# auto_attr :user, type: User # Calls User.new(**hash_data)
|
|
50
|
+
#
|
|
51
|
+
# # Arrays of custom classes
|
|
52
|
+
# auto_attr :users, type: User, array: true # Maps each hash to User.new(**hash)
|
|
53
|
+
module AutoInitializable
|
|
54
|
+
extend T::Sig
|
|
55
|
+
extend T::Helpers
|
|
56
|
+
|
|
57
|
+
# Class methods added when AutoInitializable is included.
|
|
58
|
+
#
|
|
59
|
+
# These methods provide the DSL for declaring auto-initialized attributes
|
|
60
|
+
# and generating the corresponding initialization logic.
|
|
61
|
+
module ClassMethods
|
|
62
|
+
extend T::Sig
|
|
63
|
+
|
|
64
|
+
# Declares an attribute that will be automatically initialized and typed.
|
|
65
|
+
#
|
|
66
|
+
# @param name [Symbol] The name of the attribute
|
|
67
|
+
# @param type [Class, T.untyped] The type of the attribute (String, Integer, Hash, custom classes, etc.)
|
|
68
|
+
# @param from [Symbol, nil] The source key in initialization hash (defaults to name)
|
|
69
|
+
# @param array [Boolean] Whether this attribute is an array of the specified type
|
|
70
|
+
# @param nilable [Boolean] Whether this attribute can be nil
|
|
71
|
+
#
|
|
72
|
+
# @example Basic attributes
|
|
73
|
+
# auto_attr :name, type: String, nilable: false
|
|
74
|
+
# auto_attr :email, type: String
|
|
75
|
+
# auto_attr :age, type: Integer
|
|
76
|
+
#
|
|
77
|
+
# @example Arrays
|
|
78
|
+
# auto_attr :tags, type: String, array: true
|
|
79
|
+
# auto_attr :users, type: User, array: true
|
|
80
|
+
#
|
|
81
|
+
# @example Custom key mapping
|
|
82
|
+
# auto_attr :display_name, type: String, from: :display
|
|
83
|
+
#
|
|
84
|
+
# @example Complex types
|
|
85
|
+
# auto_attr :metadata, type: Hash
|
|
86
|
+
# auto_attr :profile, type: Profile
|
|
87
|
+
#
|
|
88
|
+
# @note Basic Ruby types (Hash, Array, String, Integer, Float) are preserved without conversion.
|
|
89
|
+
# Custom classes will be instantiated from hash data using Class.new(**hash).
|
|
90
|
+
sig do
|
|
91
|
+
params(
|
|
92
|
+
name: Symbol,
|
|
93
|
+
type: T.untyped,
|
|
94
|
+
from: T.nilable(Symbol),
|
|
95
|
+
array: T::Boolean,
|
|
96
|
+
nilable: T::Boolean
|
|
97
|
+
).void
|
|
98
|
+
end
|
|
99
|
+
def auto_attr(name, type: T.untyped, from: nil, array: false, nilable: true)
|
|
100
|
+
@auto_attrs ||= {}
|
|
101
|
+
@auto_attrs[name] = {
|
|
102
|
+
type: type,
|
|
103
|
+
from: from || name,
|
|
104
|
+
array: array,
|
|
105
|
+
nilable: nilable
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Generates the initialize method and typed attr_readers based on declared auto_attr calls.
|
|
110
|
+
#
|
|
111
|
+
# This method must be called after all auto_attr declarations to generate the actual
|
|
112
|
+
# initialization code and Sorbet-typed attribute readers.
|
|
113
|
+
#
|
|
114
|
+
# @example
|
|
115
|
+
# class User
|
|
116
|
+
# include AutoInitializable
|
|
117
|
+
#
|
|
118
|
+
# auto_attr :name, type: String
|
|
119
|
+
# auto_attr :age, type: Integer
|
|
120
|
+
#
|
|
121
|
+
# auto_initialize! # Must be called after auto_attr declarations
|
|
122
|
+
# end
|
|
123
|
+
#
|
|
124
|
+
# @note This method generates:
|
|
125
|
+
# - An initialize method that accepts keyword arguments
|
|
126
|
+
# - Type conversion for nested objects
|
|
127
|
+
# - Sorbet-typed attr_reader methods
|
|
128
|
+
# - Automatic validation that required attributes are provided
|
|
129
|
+
sig { void }
|
|
130
|
+
def auto_initialize!
|
|
131
|
+
attrs = @auto_attrs || {}
|
|
132
|
+
klass = self
|
|
133
|
+
|
|
134
|
+
T.unsafe(self).define_method :initialize do |**kwargs|
|
|
135
|
+
attrs.each do |name, config|
|
|
136
|
+
source_key = config[:from]
|
|
137
|
+
value = kwargs[source_key]
|
|
138
|
+
|
|
139
|
+
# Skip if nil and nilable
|
|
140
|
+
if value.nil?
|
|
141
|
+
T.unsafe(self).instance_variable_set("@#{name}", nil)
|
|
142
|
+
next
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Handle type conversion
|
|
146
|
+
converted_value = if config[:type]
|
|
147
|
+
if config[:array] && value.is_a?(Array)
|
|
148
|
+
# Handle array of objects
|
|
149
|
+
value.map do |item|
|
|
150
|
+
T.unsafe(klass).send(:convert_value, item, config[:type])
|
|
151
|
+
end
|
|
152
|
+
else
|
|
153
|
+
T.unsafe(klass).send(:convert_value, value, config[:type])
|
|
154
|
+
end
|
|
155
|
+
else
|
|
156
|
+
value
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
T.unsafe(self).instance_variable_set("@#{name}", converted_value)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Call parent initializer if it exists and has an initialize method
|
|
163
|
+
return unless T.unsafe(self).class.superclass.method_defined?(:initialize)
|
|
164
|
+
|
|
165
|
+
super()
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Create simple attr_readers
|
|
169
|
+
# Type signatures are provided by generated RBI files
|
|
170
|
+
attrs.each_key do |name|
|
|
171
|
+
T.unsafe(self).attr_reader name
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Returns the hash of declared attributes and their configuration.
|
|
176
|
+
#
|
|
177
|
+
# @return [Hash<Symbol, Hash>] Hash mapping attribute names to their configuration
|
|
178
|
+
#
|
|
179
|
+
# @example
|
|
180
|
+
# User.auto_attrs
|
|
181
|
+
# # => {
|
|
182
|
+
# # name: { type: String, from: :name, array: false, nilable: false },
|
|
183
|
+
# # email: { type: String, from: :email, array: false, nilable: true }
|
|
184
|
+
# # }
|
|
185
|
+
sig { returns(T::Hash[Symbol, T.untyped]) }
|
|
186
|
+
def auto_attrs
|
|
187
|
+
@auto_attrs || {}
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
# Converts a value to the specified type if appropriate.
|
|
193
|
+
#
|
|
194
|
+
# This method handles type conversion for nested objects during initialization.
|
|
195
|
+
# Basic Ruby types (Hash, Array, String, etc.) are preserved as-is to avoid
|
|
196
|
+
# unwanted conversions. Custom classes are instantiated from hash data.
|
|
197
|
+
#
|
|
198
|
+
# @param value [Object] The value to potentially convert
|
|
199
|
+
# @param type [Class] The target type for conversion
|
|
200
|
+
# @return [Object] The converted value or original value if no conversion needed
|
|
201
|
+
#
|
|
202
|
+
# @example
|
|
203
|
+
# convert_value({ name: "John" }, User) # => User.new(name: "John")
|
|
204
|
+
# convert_value({ key: "value" }, Hash) # => { key: "value" } (unchanged)
|
|
205
|
+
# convert_value("text", String) # => "text" (unchanged)
|
|
206
|
+
sig { params(value: T.untyped, type: T.untyped).returns(T.untyped) }
|
|
207
|
+
def convert_value(value, type)
|
|
208
|
+
return value unless value.is_a?(Hash)
|
|
209
|
+
|
|
210
|
+
# Don't try to convert basic Ruby types like Hash, Array, String, etc.
|
|
211
|
+
return value if [Hash, Array, String, Integer, Float, TrueClass, FalseClass].include?(type)
|
|
212
|
+
|
|
213
|
+
# Check if type is a class that responds to new (for our custom classes)
|
|
214
|
+
if type.is_a?(Class) && type.respond_to?(:new)
|
|
215
|
+
type.new(**value)
|
|
216
|
+
else
|
|
217
|
+
value
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
mixes_in_class_methods(ClassMethods)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# typed: true
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Prusalink
|
|
5
|
+
# Base class for all API resources.
|
|
6
|
+
class BaseResource
|
|
7
|
+
class << self
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
# Defines the path for the resource's endpoint
|
|
11
|
+
#
|
|
12
|
+
# @param [String] path The API's endpoint path
|
|
13
|
+
sig { params(path: String).void }
|
|
14
|
+
def resource_path(path)
|
|
15
|
+
@path = path
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Gets a single resource and instantiates the object.
|
|
19
|
+
#
|
|
20
|
+
# @param path [String, nil] appended to the resource path
|
|
21
|
+
# @param deserialize [Boolean] whether to build an object from the response
|
|
22
|
+
# @param options [Hash] query string parameters
|
|
23
|
+
# @return [BaseResource, Hash, nil] nil when the printer answers 204 No Content
|
|
24
|
+
def fetch_resource(path = nil, deserialize: true, options: {})
|
|
25
|
+
path = [@path, path].compact.join("/")
|
|
26
|
+
path += "?#{Faraday::Utils.build_query(options)}" if options.any?
|
|
27
|
+
response = client.request(path, http_method: :get)
|
|
28
|
+
return nil if response.nil?
|
|
29
|
+
return deserialize(response) if deserialize
|
|
30
|
+
|
|
31
|
+
response
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Sends a POST request to the resource's endpoint.
|
|
35
|
+
sig do
|
|
36
|
+
params(path: String, params: T.untyped, headers: T::Hash[T.any(String, Symbol), String]).returns(T.untyped)
|
|
37
|
+
end
|
|
38
|
+
def post(path: @path, params: nil, headers: {})
|
|
39
|
+
client.request(path, http_method: :post, body: params, headers: headers)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Sends a PUT request to the resource's endpoint. `params` may be a Hash (sent as JSON),
|
|
43
|
+
# a String or an IO (sent as-is).
|
|
44
|
+
sig do
|
|
45
|
+
params(path: String, params: T.untyped, headers: T::Hash[T.any(String, Symbol), String]).returns(T.untyped)
|
|
46
|
+
end
|
|
47
|
+
def put(path: @path, params: nil, headers: {})
|
|
48
|
+
client.request(path, http_method: :put, body: params, headers: headers)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Sends a PATCH request to the resource's endpoint.
|
|
52
|
+
sig do
|
|
53
|
+
params(path: String, params: T.untyped, headers: T::Hash[T.any(String, Symbol), String]).returns(T.untyped)
|
|
54
|
+
end
|
|
55
|
+
def patch(path: @path, params: nil, headers: {})
|
|
56
|
+
client.request(path, http_method: :patch, body: params, headers: headers)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Sends a DELETE request to the resource's endpoint.
|
|
60
|
+
sig { params(path: String, headers: T::Hash[T.any(String, Symbol), String]).returns(T.untyped) }
|
|
61
|
+
def delete(path: @path, headers: {})
|
|
62
|
+
client.request(path, http_method: :delete, headers: headers)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Sends a HEAD request to the resource's endpoint and returns the response headers.
|
|
66
|
+
sig { params(path: String).returns(T::Hash[String, String]) }
|
|
67
|
+
def head(path: @path)
|
|
68
|
+
client.head(path)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Instantiates an object from a hash. Can be overridden by child classes
|
|
72
|
+
#
|
|
73
|
+
# @param [Hash] attrs the object's attributes
|
|
74
|
+
# @return [BaseResource]
|
|
75
|
+
def deserialize(attrs)
|
|
76
|
+
new(**attrs)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
sig { returns(Client) }
|
|
82
|
+
def client
|
|
83
|
+
client = Prusalink.client
|
|
84
|
+
raise Exceptions::NotConfiguredError, "No client configured. Use Prusalink.configure" unless client
|
|
85
|
+
|
|
86
|
+
client
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def initialize(**attrs); end
|
|
91
|
+
end
|
|
92
|
+
end
|