Class: Cloudflare::Resource

Inherits:
Object
  • Object
show all
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

Constant Summary collapse

ENVELOPE_KEYS =

Cloudflare uses two response envelope shapes:

- V4 API: { result: ..., success, errors, messages }    
%w[result data].freeze

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(response, scope: {}) ⇒ Resource

Returns a new instance of Resource.

Parameters:

  • (response)
  • scope: (scope_hash) (defaults to: {})


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.

Returns:

  • (Hash[Symbol, untyped])


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.

Returns:

  • (String, nil)


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.

Returns:

  • (String, nil)


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.

Returns:

  • (scope_hash)


250
251
252
# File 'lib/cloudflare/resource.rb', line 250

def scope
  @scope
end

Class Method Details

.all(**params) ⇒ Array[Resource]

Parameters:

  • params (Object)

Returns:



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.

Parameters:

  • name (Symbol)
  • type (Object) (defaults to: nil)

Returns:

  • (Symbol)


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]

Returns:

  • (Hash[Symbol, untyped])


87
# File 'lib/cloudflare/resource.rb', line 87

def attributes = @_attributes ||= {}

.collection_path(path = nil) ⇒ String?

Parameters:

  • path (String, nil) (defaults to: nil)

Returns:

  • (String, nil)


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.

Parameters:

  • attrs (Object)

Returns:



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

Parameters:

  • id (String)
  • scope_attrs (Object)

Returns:



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".

Parameters:

  • name (Symbol)
  • class_name: (String, nil) (defaults to: nil)

Returns:

  • (Symbol)


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.

Parameters:

  • name (Symbol)
  • class_name: (String, nil) (defaults to: nil)

Returns:

  • (Symbol)


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?

Parameters:

  • path (String, nil) (defaults to: nil)

Returns:

  • (String, nil)


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

Returns:

  • (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]

Returns:

  • (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]

Parameters:

  • keys (Symbol)

Returns:

  • (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?

Parameters:

  • other (Object)

Returns:

  • (Boolean)


297
298
299
# File 'lib/cloudflare/resource.rb', line 297

def ==(other)
  other.is_a?(self.class) && other.id == id
end

#[](key) ⇒ Object

Parameters:

  • key (Symbol, String)

Returns:

  • (Object)


265
# File 'lib/cloudflare/resource.rb', line 265

def [](key)     = (ensure_loaded!; @attrs[key.to_s])

#attributes ⇒ response

Returns:

  • (response)


267
# File 'lib/cloudflare/resource.rb', line 267

def attributes  = (ensure_loaded!; @attrs)

#destroy ⇒ self

Returns:

  • (self)


275
276
277
278
# File 'lib/cloudflare/resource.rb', line 275

def destroy
  request(:delete, member_path)
  freeze
end

#hash ⇒ Integer

Returns:

  • (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.

Returns:

  • (String, nil)


264
# File 'lib/cloudflare/resource.rb', line 264

def id          = @attrs["id"]

#reload ⇒ self

Returns:

  • (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

Returns:

  • (response)


266
# File 'lib/cloudflare/resource.rb', line 266

def to_h        = (ensure_loaded!; @attrs)

#update(**changes) ⇒ self

Parameters:

  • changes (Object)

Returns:

  • (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