Class: Cloudflare::Resource
- Inherits:
-
Object
- Object
- Cloudflare::Resource
- Defined in:
- lib/cloudflare/resource.rb
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, messages } — most products - RealtimeKit (Dyte heritage): { data: ..., success } %w[result data].freeze
Class Attribute Summary collapse
-
._attributes ⇒ Object
readonly
Returns the value of attribute _attributes.
-
._collection_path ⇒ Object
readonly
Returns the value of attribute _collection_path.
-
._member_path ⇒ Object
readonly
Returns the value of attribute _member_path.
Instance Attribute Summary collapse
-
#scope ⇒ Object
readonly
Returns the value of attribute scope.
Class Method Summary collapse
- .all(**params) ⇒ Object
-
.attribute(name, type = nil, wire_name: nil) ⇒ Object
Declare a typed attribute reader.
- .attributes ⇒ Object
- .collection_path(path = nil) ⇒ Object
-
.create(**attrs) ⇒ Object
CRUD defaults.
- .find(id, **scope_attrs) ⇒ Object
-
.has_many(name, class_name: nil) ⇒ Object
Declare a nested collection reachable via REST sub-path.
-
.has_one(name, class_name: nil) ⇒ Object
Declare a nested singleton sub-resource (no id, fixed sub-path).
- .member_path(path = nil) ⇒ Object
-
.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
- .scope_params ⇒ Object
- .scope_required(*keys) ⇒ Object
-
.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 (‘result` for V4, `data` for 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) ⇒ Object (also: #eql?)
- #[](key) ⇒ Object
- #attributes ⇒ Object
- #destroy ⇒ Object
- #hash ⇒ Object
-
#id ⇒ Object
Note:
#idis intentionally not gated throughensure_loaded!. -
#initialize(response, scope: {}) ⇒ Resource
constructor
A new instance of Resource.
- #reload ⇒ Object
-
#set_attrs_from_response(response) ⇒ Object
Replace @attrs from a freshly-received response and mark the instance loaded.
- #to_h ⇒ Object
- #update(**changes) ⇒ Object
Constructor Details
#initialize(response, scope: {}) ⇒ Resource
Returns a new instance of Resource.
211 212 213 214 215 |
# File 'lib/cloudflare/resource.rb', line 211 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 ⇒ Object (readonly)
Returns the value of attribute _attributes.
24 25 26 |
# File 'lib/cloudflare/resource.rb', line 24 def _attributes @_attributes end |
._collection_path ⇒ Object (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 ⇒ Object (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 ⇒ Object (readonly)
Returns the value of attribute scope.
209 210 211 |
# File 'lib/cloudflare/resource.rb', line 209 def scope @scope end |
Class Method Details
.all(**params) ⇒ Object
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 = Connection.instance.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) ⇒ Object
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 ⇒ Object
87 |
# File 'lib/cloudflare/resource.rb', line 87 def attributes = @_attributes ||= {} |
.collection_path(path = nil) ⇒ Object
26 27 28 |
# File 'lib/cloudflare/resource.rb', line 26 def collection_path(path = nil) path ? @_collection_path = path : @_collection_path end |
.create(**attrs) ⇒ Object
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 = Connection.instance.request(:post, path, body: to_wire_keys(attrs)) new(response, scope: scope) end |
.find(id, **scope_attrs) ⇒ Object
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 = Connection.instance.request(:get, path) new(response, scope: scope) end |
.has_many(name, class_name: nil) ⇒ Object
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) ⇒ Object
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) ⇒ Object
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 |
.scope_params ⇒ Object
38 39 40 |
# File 'lib/cloudflare/resource.rb', line 38 def scope_params ((@_explicit_scope || []) + [ :account_id ]).uniq end |
.scope_required(*keys) ⇒ Object
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) ⇒ Object Also known as: eql?
256 257 258 |
# File 'lib/cloudflare/resource.rb', line 256 def ==(other) other.is_a?(self.class) && other.id == id end |
#[](key) ⇒ Object
224 |
# File 'lib/cloudflare/resource.rb', line 224 def [](key) = (ensure_loaded!; @attrs[key.to_s]) |
#attributes ⇒ Object
226 |
# File 'lib/cloudflare/resource.rb', line 226 def attributes = (ensure_loaded!; @attrs) |
#destroy ⇒ Object
234 235 236 237 |
# File 'lib/cloudflare/resource.rb', line 234 def destroy Connection.instance.request(:delete, member_path) freeze end |
#hash ⇒ Object
261 |
# File 'lib/cloudflare/resource.rb', line 261 def hash = [ self.class, id ].hash |
#id ⇒ Object
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.
223 |
# File 'lib/cloudflare/resource.rb', line 223 def id = @attrs["id"] |
#reload ⇒ Object
239 240 241 242 243 |
# File 'lib/cloudflare/resource.rb', line 239 def reload response = Connection.instance.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.
250 251 252 253 254 |
# File 'lib/cloudflare/resource.rb', line 250 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 ⇒ Object
225 |
# File 'lib/cloudflare/resource.rb', line 225 def to_h = (ensure_loaded!; @attrs) |
#update(**changes) ⇒ Object
228 229 230 231 232 |
# File 'lib/cloudflare/resource.rb', line 228 def update(**changes) response = Connection.instance.request(:patch, member_path, body: self.class.to_wire_keys(changes)) set_attrs_from_response(response) self end |