Class: Clacky::ApiExtension
- Inherits:
-
Object
- Object
- Clacky::ApiExtension
show all
- Defined in:
- lib/clacky/api_extension.rb
Overview
Base class for user-defined HTTP API extensions loaded from ~/.clacky/api_ext/<name>/handler.rb. Subclasses use a tiny route DSL (get/post/put/patch/delete) to expose endpoints under
/api/ext/<name>/<sub-path>
The framework wires up access-key auth, timeouts, JSON error envelopes, path-parameter parsing, and a curated handler context — extension authors only fill in business logic.
Minimal example (~/.clacky/api_ext/my-dashboard/handler.rb):
class MyDashboardExt < Clacky::ApiExtension
get "/summary" do
json(sessions: session_manager.list.size)
end
end
Mounted automatically at: GET /api/ext/my-dashboard/summary
Defined Under Namespace
Classes: Halt, Route, ScopedLogger
Constant Summary
collapse
- HTTP_METHODS =
%i[get post put patch delete].freeze
- MAX_TIMEOUT =
600
- DEFAULT_TIMEOUT =
10
Instance Attribute Summary collapse
Class Method Summary
collapse
Instance Method Summary
collapse
Constructor Details
#initialize(req:, res:, route:, params:, http_server:) ⇒ ApiExtension
Returns a new instance of ApiExtension.
165
166
167
168
169
170
171
|
# File 'lib/clacky/api_extension.rb', line 165
def initialize(req:, res:, route:, params:, http_server:)
@req = req
@res = res
@route = route
@params = params
@http_server = http_server
end
|
Instance Attribute Details
#params ⇒ Object
Returns the value of attribute params.
163
164
165
|
# File 'lib/clacky/api_extension.rb', line 163
def params
@params
end
|
#req ⇒ Object
Returns the value of attribute req.
163
164
165
|
# File 'lib/clacky/api_extension.rb', line 163
def req
@req
end
|
#res ⇒ Object
Returns the value of attribute res.
163
164
165
|
# File 'lib/clacky/api_extension.rb', line 163
def res
@res
end
|
#route ⇒ Object
Returns the value of attribute route.
163
164
165
|
# File 'lib/clacky/api_extension.rb', line 163
def route
@route
end
|
Class Method Details
.class_timeout ⇒ Object
76
77
78
|
# File 'lib/clacky/api_extension.rb', line 76
def class_timeout
@class_timeout
end
|
.compile_pattern(pattern) ⇒ Object
153
154
155
156
157
158
159
160
|
# File 'lib/clacky/api_extension.rb', line 153
def compile_pattern(pattern)
param_names = []
regex_str = pattern.gsub(%r{:([a-zA-Z_][a-zA-Z0-9_]*)}) do |_match|
param_names << Regexp.last_match(1).to_sym
"([^/]+)"
end
[Regexp.new("\\A#{regex_str}\\z"), param_names]
end
|
.ext_dir ⇒ Object
92
93
94
|
# File 'lib/clacky/api_extension.rb', line 92
def ext_dir
@ext_dir
end
|
.ext_dir=(value) ⇒ Object
96
97
98
|
# File 'lib/clacky/api_extension.rb', line 96
def ext_dir=(value)
@ext_dir = value
end
|
.ext_id ⇒ Object
84
85
86
|
# File 'lib/clacky/api_extension.rb', line 84
def ext_id
@ext_id
end
|
.ext_id=(value) ⇒ Object
88
89
90
|
# File 'lib/clacky/api_extension.rb', line 88
def ext_id=(value)
@ext_id = value
end
|
.inherited(subclass) ⇒ Object
100
101
102
|
# File 'lib/clacky/api_extension.rb', line 100
def meta
@meta ||= {}
end
|
104
105
106
|
# File 'lib/clacky/api_extension.rb', line 104
def meta=(value)
@meta = value || {}
end
|
.normalize_pattern(pattern) ⇒ Object
146
147
148
149
150
151
|
# File 'lib/clacky/api_extension.rb', line 146
def normalize_pattern(pattern)
pattern = pattern.to_s
pattern = "/#{pattern}" unless pattern.start_with?("/")
pattern = pattern.chomp("/")
pattern.empty? ? "/" : pattern
end
|
.pending_subclasses ⇒ Object
Captures every subclass at the moment its ‘class` body finishes being required — the loader pops the most recent one off this list to bind an ext_id/dir without relying on ObjectSpace scans.
62
63
64
|
# File 'lib/clacky/api_extension.rb', line 62
def pending_subclasses
@pending_subclasses ||= []
end
|
.public_endpoint(pattern) ⇒ Object
Mark a route as not requiring access-key auth. Caller must also declare ‘public: true` in meta.yml for the framework to honor this.
119
120
121
|
# File 'lib/clacky/api_extension.rb', line 119
def public_endpoint(pattern)
public_paths << normalize_pattern(pattern)
end
|
.public_paths ⇒ Object
80
81
82
|
# File 'lib/clacky/api_extension.rb', line 80
def public_paths
@public_paths ||= []
end
|
.register(ext_id, klass) ⇒ Object
50
51
52
|
# File 'lib/clacky/api_extension.rb', line 50
def register(ext_id, klass)
registry[ext_id] = klass
end
|
.registry ⇒ Object
Registry of all loaded ApiExtension subclasses, keyed by extension id (== directory name == mount prefix segment).
46
47
48
|
# File 'lib/clacky/api_extension.rb', line 46
def registry
@registry ||= {}
end
|
.reset_registry! ⇒ Object
54
55
56
57
|
# File 'lib/clacky/api_extension.rb', line 54
def reset_registry!
@registry = {}
@pending_subclasses = []
end
|
.routes ⇒ Object
Per-subclass state — inherited classes carry their own routes/options.
72
73
74
|
# File 'lib/clacky/api_extension.rb', line 72
def routes
@routes ||= []
end
|
.timeout(seconds) ⇒ Object
Set a default timeout (seconds) for every handler in this class. Per-route override available via ‘get “/x”, timeout: 30 do … end`.
110
111
112
113
114
115
|
# File 'lib/clacky/api_extension.rb', line 110
def timeout(seconds)
raise ArgumentError, "timeout must be > 0" unless seconds.is_a?(Numeric) && seconds > 0
raise ArgumentError, "timeout exceeds MAX_TIMEOUT (#{MAX_TIMEOUT}s)" if seconds > MAX_TIMEOUT
@class_timeout = seconds.to_f
end
|
Instance Method Details
#agent_config ⇒ Object
237
238
239
|
# File 'lib/clacky/api_extension.rb', line 237
def agent_config
@http_server&.instance_variable_get(:@agent_config)
end
|
#config ⇒ Object
229
230
231
|
# File 'lib/clacky/api_extension.rb', line 229
def config
self.class.meta["config"] || {}
end
|
#data_path(*parts) ⇒ Object
215
216
217
218
219
|
# File 'lib/clacky/api_extension.rb', line 215
def data_path(*parts)
base = File.join(self.class.ext_dir, "data")
FileUtils.mkdir_p(base)
File.join(base, *parts.map(&:to_s))
end
|
#error!(message, status: 400, **extra) ⇒ Object
196
197
198
199
200
|
# File 'lib/clacky/api_extension.rb', line 196
def error!(message, status: 400, **)
payload = { error: message.to_s }
payload.merge!() unless .empty?
raise Halt.new(status, JSON.generate(payload), "application/json; charset=utf-8")
end
|
#ext_dir ⇒ Object
221
222
223
|
# File 'lib/clacky/api_extension.rb', line 221
def ext_dir
self.class.ext_dir
end
|
#ext_id ⇒ Object
225
226
227
|
# File 'lib/clacky/api_extension.rb', line 225
def ext_id
self.class.ext_id
end
|
#invoke ⇒ Object
173
174
175
|
# File 'lib/clacky/api_extension.rb', line 173
def invoke
instance_exec(&route.block)
end
|
#json(*args, **kwargs) ⇒ Object
—- handler context (white-listed access to host process) —-
179
180
181
182
183
184
185
186
187
188
189
190
|
# File 'lib/clacky/api_extension.rb', line 179
def json(*args, **kwargs)
if args.empty?
raise Halt.new(200, JSON.generate(kwargs), "application/json; charset=utf-8")
elsif args.size == 1
status = kwargs[:status] || 200
raise Halt.new(status, JSON.generate(args[0]), "application/json; charset=utf-8")
else
raise ArgumentError, "json: expected (hash) or (key: value, ...)"
end
end
|
#json_body ⇒ Object
202
203
204
205
206
207
208
209
|
# File 'lib/clacky/api_extension.rb', line 202
def json_body
@json_body ||= begin
return {} if req.body.nil? || req.body.empty?
JSON.parse(req.body)
rescue JSON::ParserError
{}
end
end
|
#logger ⇒ Object
245
246
247
|
# File 'lib/clacky/api_extension.rb', line 245
def logger
@logger ||= ScopedLogger.new(self.class.ext_id)
end
|
#query ⇒ Object
211
212
213
|
# File 'lib/clacky/api_extension.rb', line 211
def query
@query ||= req.query || {}
end
|
#server_start_time ⇒ Object
241
242
243
|
# File 'lib/clacky/api_extension.rb', line 241
def server_start_time
@http_server&.instance_variable_get(:@start_time)
end
|
#session_manager ⇒ Object
233
234
235
|
# File 'lib/clacky/api_extension.rb', line 233
def session_manager
@http_server&.instance_variable_get(:@session_manager)
end
|
#text(str, status: 200) ⇒ Object
192
193
194
|
# File 'lib/clacky/api_extension.rb', line 192
def text(str, status: 200)
raise Halt.new(status, str.to_s, "text/plain; charset=utf-8")
end
|