Class: Cloudflare::Resource
- Inherits:
-
Object
- Object
- Cloudflare::Resource
- Defined in:
- lib/cloudflare/resource.rb,
sig/cloudflare/resource.rbs
Overview
Base class for every Cloudflare resource — Meeting, Bucket, Zone, Record,
Script, etc. Provides Active-Record-flavored CRUD on top of REST endpoints,
plus an attribute macro for explicit, source-visible attribute readers.
Subclasses declare paths and attributes:
class Cloudflare::RealtimeKit::Meeting < Cloudflare::Resource
collection_path "/accounts/{account_id}/realtime/kit/{app_id}/meetings"
member_path "/accounts/{account_id}/realtime/kit/{app_id}/meetings/{id}"
scope_required :app_id
attribute :id, String
attribute :title, String
attribute :location_hint, String, wire_name: "locationHint"
end
Direct Known Subclasses
Cloudflare::RealtimeKit::ActiveLivestreamSession, Cloudflare::RealtimeKit::ActiveSession, Cloudflare::RealtimeKit::App, Cloudflare::RealtimeKit::Chat, Cloudflare::RealtimeKit::Livestream, Cloudflare::RealtimeKit::LivestreamSession, Cloudflare::RealtimeKit::Meeting, Cloudflare::RealtimeKit::Participant, Cloudflare::RealtimeKit::Preset, Cloudflare::RealtimeKit::Recording, Cloudflare::RealtimeKit::Session, Cloudflare::RealtimeKit::SessionParticipant, Cloudflare::RealtimeKit::Summary, Cloudflare::RealtimeKit::Transcript, Cloudflare::RealtimeKit::Webhook
Constant Summary collapse
- ENVELOPE_KEYS =
Cloudflare uses two response envelope shapes:
- V4 API: { result: ..., success, errors, } %w[result data].freeze
Class Attribute Summary collapse
-
._attributes ⇒ Hash[Symbol, untyped]
readonly
Returns the value of attribute _attributes.
-
._collection_path ⇒ String?
readonly
Returns the value of attribute _collection_path.
-
._member_path ⇒ String?
readonly
Returns the value of attribute _member_path.
Instance Attribute Summary collapse
-
#scope ⇒ scope_hash
readonly
Returns the value of attribute scope.
Class Method Summary collapse
- .all(**params) ⇒ Array[Resource]
-
.attribute(name, type = nil, wire_name: nil) ⇒ Symbol
Declare a typed attribute reader.
- .attributes ⇒ Hash[Symbol, untyped]
- .collection_path(path = nil) ⇒ String?
-
.configured_api_token ⇒ Object
Token resolution: per-product accessor (e.g.,
Cloudflare::RealtimeKit.api_token) if set, elseCloudflare.api_token. -
.create(**attrs) ⇒ Resource
CRUD defaults.
- .find(id, **scope_attrs) ⇒ Resource
-
.has_many(name, class_name: nil) ⇒ Symbol
Declare a nested collection reachable via REST sub-path.
-
.has_one(name, class_name: nil) ⇒ Symbol
Declare a nested singleton sub-resource (no id, fixed sub-path).
- .member_path(path = nil) ⇒ String?
-
.read_only ⇒ Object
Mark this resource as read-only — i.e., the upstream spec exposes only GET endpoints and no POST/PATCH/PUT/DELETE.
- .read_only? ⇒ Boolean
-
.request(method, path, **opts) ⇒ Object
Wraps
Connection.instance.requestwith the api_token resolved from the resource's product namespace. - .scope_params ⇒ Array[Symbol]
- .scope_required(*keys) ⇒ Array[Symbol]
-
.to_wire_keys(hash) ⇒ Object
Translate a kwargs Hash from Ruby snake_case keys to wire-format keys for SENDING in a request body or query.
-
.unwrap_envelope(response) ⇒ Object
Unwrap a Cloudflare response envelope (
resultfor V4,datafor RealtimeKit) and return the inner payload. -
.wire_kwarg(ruby_name, wire_name) ⇒ Object
Records the on-the-wire field name for a request body kwarg.
-
.wire_name_for_request(ruby_key) ⇒ Object
Wire name used when sending a kwarg in a request body or query.
-
.wire_name_for_response(ruby_key) ⇒ Object
Wire name used when reading from a response.
Instance Method Summary collapse
- #==(other) ⇒ Boolean (also: #eql?)
- #[](key) ⇒ Object
- #attributes ⇒ response
- #destroy ⇒ self
- #hash ⇒ Integer
-
#id ⇒ String?
Note:
#idis intentionally not gated throughensure_loaded!. -
#initialize(response, scope: {}) ⇒ Resource
constructor
A new instance of Resource.
- #reload ⇒ self
-
#set_attrs_from_response(response) ⇒ Object
Replace
@attrsfrom a freshly-received response and mark the instance loaded. - #to_h ⇒ response
- #update(**changes) ⇒ self
Constructor Details
#initialize(response, scope: {}) ⇒ Resource
Returns a new instance of Resource.
252 253 254 255 256 |
# File 'lib/cloudflare/resource.rb', line 252 def initialize(response, scope: {}) @attrs = self.class.unwrap_envelope(response).then { |r| r.is_a?(Hash) ? r.transform_keys(&:to_s) : {} } @scope = scope @loaded = !@attrs.empty? end |
Class Attribute Details
._attributes ⇒ Hash[Symbol, untyped] (readonly)
Returns the value of attribute _attributes.
24 25 26 |
# File 'lib/cloudflare/resource.rb', line 24 def _attributes @_attributes end |
._collection_path ⇒ String? (readonly)
Returns the value of attribute _collection_path.
24 25 26 |
# File 'lib/cloudflare/resource.rb', line 24 def _collection_path @_collection_path end |
._member_path ⇒ String? (readonly)
Returns the value of attribute _member_path.
24 25 26 |
# File 'lib/cloudflare/resource.rb', line 24 def _member_path @_member_path end |
Instance Attribute Details
#scope ⇒ scope_hash (readonly)
Returns the value of attribute scope.
250 251 252 |
# File 'lib/cloudflare/resource.rb', line 250 def scope @scope end |
Class Method Details
.all(**params) ⇒ Array[Resource]
174 175 176 177 178 179 180 |
# File 'lib/cloudflare/resource.rb', line 174 def all(**params) scope = extract_scope!(params) path = interpolate(_collection_path, scope) response = request(:get, path, params: to_wire_keys(params)) items = unwrap_envelope(response) Array(items).map { new(_1, scope: scope) } end |
.attribute(name, type = nil, wire_name: nil) ⇒ Symbol
Declare a typed attribute reader. Coerces on read for known types
(Time, Integer, Float). For :boolean, also defines a name? predicate.
Use wire_name when the on-the-wire field name differs from the
snake_case Ruby identifier (e.g., camelCase fields like locationHint).
Readers gate through ensure_loaded!: a stub created by has_one
auto-fetches its attrs on the first attribute read. Action methods
(+kick+, mute, etc.) skip the gate, so action-only flows never pay
for an unwanted GET.
72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/cloudflare/resource.rb', line 72 def attribute(name, type = nil, wire_name: nil) key = name.to_sym wire_key = wire_name&.to_s || name.to_s (@_attributes ||= {})[key] = { type: type, wire_name: wire_key } # +#id+ is special — it's used internally by +member_path+ which # +reload+ depends on, so a gated reader would cause infinite # recursion. Subclasses can still declare +attribute :id, String+ # for type documentation; we just don't override the base reader. return if key == :id define_method(name) { ensure_loaded!; coerce_attribute(@attrs[wire_key], type) } define_method("#{name}?") { ensure_loaded!; !!@attrs[wire_key] } if type == :boolean end |
.attributes ⇒ Hash[Symbol, untyped]
87 |
# File 'lib/cloudflare/resource.rb', line 87 def attributes = @_attributes ||= {} |
.collection_path(path = nil) ⇒ String?
26 27 28 |
# File 'lib/cloudflare/resource.rb', line 26 def collection_path(path = nil) path ? @_collection_path = path : @_collection_path end |
.configured_api_token ⇒ Object
Token resolution: per-product accessor (e.g., Cloudflare::RealtimeKit.api_token)
if set, else Cloudflare.api_token. Connection raises when both are nil.
194 195 196 197 |
# File 'lib/cloudflare/resource.rb', line 194 def configured_api_token ns = product_namespace (ns && ns.respond_to?(:api_token) && ns.api_token) || Cloudflare.api_token end |
.create(**attrs) ⇒ Resource
CRUD defaults. Subclasses override to add explicit kwargs.
160 161 162 163 164 165 |
# File 'lib/cloudflare/resource.rb', line 160 def create(**attrs) scope = extract_scope!(attrs) path = interpolate(_collection_path, scope) response = request(:post, path, body: to_wire_keys(attrs)) new(response, scope: scope) end |
.find(id, **scope_attrs) ⇒ Resource
167 168 169 170 171 172 |
# File 'lib/cloudflare/resource.rb', line 167 def find(id, **scope_attrs) scope = build_scope(scope_attrs) path = interpolate(_member_path, scope.merge(id: id)) response = request(:get, path) new(response, scope: scope) end |
.has_many(name, class_name: nil) ⇒ Symbol
Declare a nested collection reachable via REST sub-path. Returns a
Relation scoped to this parent. Convention: participants →
Participant (singularize + classify) in the same product namespace.
Override with class_name: "Cloudflare::Other::Klass".
133 134 135 136 137 138 |
# File 'lib/cloudflare/resource.rb', line 133 def has_many(name, class_name: nil) define_method(name) do @relations ||= {} @relations[name.to_sym] ||= Relation.new(parent: self, model: resolve_class(name, class_name: class_name, singularize: true)) end end |
.has_one(name, class_name: nil) ⇒ Symbol
Declare a nested singleton sub-resource (no id, fixed sub-path).
Returns an unloaded stub scoped to this parent. Action methods on the
stub (e.g., active_session.kick_all) work without a fetch — they
only need scope. Attribute reads (e.g., active_session.live_participants)
auto-fetch via ensure_loaded! on first access and cache thereafter.
The cost trade-off is HTTP-aware: action-only flows pay nothing extra, read flows pay exactly one GET on first access.
148 149 150 151 152 153 154 155 156 |
# File 'lib/cloudflare/resource.rb', line 148 def has_one(name, class_name: nil) define_method(name) do @singletons ||= {} @singletons[name.to_sym] ||= begin klass = resolve_class(name, class_name: class_name, singularize: false) klass.new({}, scope: child_scope_for_nested) end end end |
.member_path(path = nil) ⇒ String?
30 31 32 |
# File 'lib/cloudflare/resource.rb', line 30 def member_path(path = nil) path ? @_member_path = path : @_member_path end |
.read_only ⇒ Object
Mark this resource as read-only — i.e., the upstream spec exposes only
GET endpoints and no POST/PATCH/PUT/DELETE. Use when a child resource
surfaced by has_many inherits a .create from Resource that would
silently 404 against the spec, e.g., SessionParticipant. Overrides
the writer methods to raise NoMethodError with a spec-citing
message before any HTTP call.
48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/cloudflare/resource.rb', line 48 def read_only @_read_only = true define_singleton_method(:create) do |**| raise NoMethodError, "#{name} is read-only — upstream has no POST endpoint" end define_method(:update) do |**| raise NoMethodError, "#{self.class.name} is read-only — upstream has no PATCH endpoint" end define_method(:destroy) do raise NoMethodError, "#{self.class.name} is read-only — upstream has no DELETE endpoint" end end |
.read_only? ⇒ Boolean
61 |
# File 'lib/cloudflare/resource.rb', line 61 def read_only? = @_read_only == true |
.request(method, path, **opts) ⇒ Object
Wraps Connection.instance.request with the api_token resolved from
the resource's product namespace. Subclasses (and instance methods,
via the same-named instance helper) call this rather than
Connection.instance.request directly so the per-product token chain
— per-call → product namespace token → top-level — is enforced
uniformly.
188 189 190 |
# File 'lib/cloudflare/resource.rb', line 188 def request(method, path, **opts) Connection.instance.request(method, path, api_token: configured_api_token, **opts) end |
.scope_params ⇒ Array[Symbol]
38 39 40 |
# File 'lib/cloudflare/resource.rb', line 38 def scope_params ((@_explicit_scope || []) + [ :account_id ]).uniq end |
.scope_required(*keys) ⇒ Array[Symbol]
34 35 36 |
# File 'lib/cloudflare/resource.rb', line 34 def scope_required(*keys) @_explicit_scope = keys.map(&:to_sym).freeze end |
.to_wire_keys(hash) ⇒ Object
Translate a kwargs Hash from Ruby snake_case keys to wire-format keys for SENDING in a request body or query.
114 115 116 |
# File 'lib/cloudflare/resource.rb', line 114 def to_wire_keys(hash) hash.each_with_object({}) { |(k, v), out| out[wire_name_for_request(k)] = v } end |
.unwrap_envelope(response) ⇒ Object
Unwrap a Cloudflare response envelope (result for V4, data for
RealtimeKit) and return the inner payload. Returns the response
unchanged when neither envelope key is present.
121 122 123 124 125 126 127 |
# File 'lib/cloudflare/resource.rb', line 121 def unwrap_envelope(response) return response unless response.is_a?(Hash) ENVELOPE_KEYS.each do |key| return response[key] if response.key?(key) && !response[key].nil? end response end |
.wire_kwarg(ruby_name, wire_name) ⇒ Object
Records the on-the-wire field name for a request body kwarg. Use this
when the request shape uses a different name than the response (e.g.,
R2 sends storageClass but reads back storage_class), or when a
kwarg isn't represented as an attribute at all (write-only fields).
93 94 95 |
# File 'lib/cloudflare/resource.rb', line 93 def wire_kwarg(ruby_name, wire_name) (@_request_wire_names ||= {})[ruby_name.to_sym] = wire_name.to_s end |
.wire_name_for_request(ruby_key) ⇒ Object
Wire name used when sending a kwarg in a request body or query. Lookup order: explicit wire_kwarg → attribute's wire_name → ruby key as string.
99 100 101 102 103 104 |
# File 'lib/cloudflare/resource.rb', line 99 def wire_name_for_request(ruby_key) sym = ruby_key.to_sym (@_request_wire_names || {})[sym] || attributes.dig(sym, :wire_name) || sym.to_s end |
.wire_name_for_response(ruby_key) ⇒ Object
Wire name used when reading from a response. Only consults the attribute declaration (response field name).
108 109 110 |
# File 'lib/cloudflare/resource.rb', line 108 def wire_name_for_response(ruby_key) attributes.dig(ruby_key.to_sym, :wire_name) || ruby_key.to_s end |
Instance Method Details
#==(other) ⇒ Boolean Also known as: eql?
297 298 299 |
# File 'lib/cloudflare/resource.rb', line 297 def ==(other) other.is_a?(self.class) && other.id == id end |
#[](key) ⇒ Object
265 |
# File 'lib/cloudflare/resource.rb', line 265 def [](key) = (ensure_loaded!; @attrs[key.to_s]) |
#attributes ⇒ response
267 |
# File 'lib/cloudflare/resource.rb', line 267 def attributes = (ensure_loaded!; @attrs) |
#destroy ⇒ self
275 276 277 278 |
# File 'lib/cloudflare/resource.rb', line 275 def destroy request(:delete, member_path) freeze end |
#hash ⇒ Integer
302 |
# File 'lib/cloudflare/resource.rb', line 302 def hash = [ self.class, id ].hash |
#id ⇒ String?
Note: #id is intentionally not gated through ensure_loaded!. It's
called inside member_path, which reload itself depends on — gating
would create a fetch loop. On a not-yet-loaded stub this returns nil;
in practice singletons (the only path that produces stubs) don't have
{id} in their member_path, so the nil is harmless during the first
reload.
264 |
# File 'lib/cloudflare/resource.rb', line 264 def id = @attrs["id"] |
#reload ⇒ self
280 281 282 283 284 |
# File 'lib/cloudflare/resource.rb', line 280 def reload response = request(:get, member_path) set_attrs_from_response(response) self end |
#set_attrs_from_response(response) ⇒ Object
Replace @attrs from a freshly-received response and mark the instance
loaded. Subclasses that issue their own writes (e.g., Recording#transition,
Summary#generate) call this so a subsequent attribute read doesn't
trigger a stale re-fetch and clobber the just-written state. Public so
subclasses can use it.
291 292 293 294 295 |
# File 'lib/cloudflare/resource.rb', line 291 def set_attrs_from_response(response) return unless response.is_a?(Hash) @attrs = self.class.unwrap_envelope(response).transform_keys(&:to_s) @loaded = true end |
#to_h ⇒ response
266 |
# File 'lib/cloudflare/resource.rb', line 266 def to_h = (ensure_loaded!; @attrs) |
#update(**changes) ⇒ self
269 270 271 272 273 |
# File 'lib/cloudflare/resource.rb', line 269 def update(**changes) response = request(:patch, member_path, body: self.class.to_wire_keys(changes)) set_attrs_from_response(response) self end |