Class: Rhino::Configuration

Inherits:
Object
  • Object
show all
Defined in:
lib/rhino/configuration.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize ⇒ Configuration

Returns a new instance of Configuration.



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# File 'lib/rhino/configuration.rb', line 24

def initialize
  @models = {}
  @model_requests = {}
  @route_groups = {}
  @multi_tenant = {
    organization_identifier_column: "id"
  }
  @invitations = {
    expires_days: 7,
    allowed_roles: nil
  }
  @nested = {
    path: "nested",
    max_operations: 50,
    allowed_models: nil
  }
  @auth = {
    enforce_group_membership: false
  }
  @test_framework = "rspec"
  @client_path = nil
  @mobile_path = nil
  @route_key = nil
  @max_scopes_per_request = 3
end

Instance Attribute Details

#auth ⇒ Object

Returns the value of attribute auth.



18
19
20
# File 'lib/rhino/configuration.rb', line 18

def auth
  @auth
end

#client_path ⇒ Object

Returns the value of attribute client_path.



5
6
7
# File 'lib/rhino/configuration.rb', line 5

def client_path
  @client_path
end

#invitations ⇒ Object

Returns the value of attribute invitations.



5
6
7
# File 'lib/rhino/configuration.rb', line 5

def invitations
  @invitations
end

#max_scopes_per_request ⇒ Object

How many client-selectable named scopes one request may combine with the bracket form (?scope=&scope[x]=1). Each scope is an arbitrary query fragment that may add joins or subqueries, so the number is capped: a request over the cap is refused with 403 "Too many scopes requested". Default 3 — a base scope, a window, and one more predicate.



17
18
19
# File 'lib/rhino/configuration.rb', line 17

def max_scopes_per_request
  @max_scopes_per_request
end

#mobile_path ⇒ Object

Returns the value of attribute mobile_path.



5
6
7
# File 'lib/rhino/configuration.rb', line 5

def mobile_path
  @mobile_path
end

#model_requests ⇒ Object (readonly)

Explicit per-model request-class registrations, kept OUT of @models so its slug => "ClassName" shape (read all over the library) is untouched. Shape: { slug_sym => { store: "ClassName" | nil, update: "ClassName" | nil } }



22
23
24
# File 'lib/rhino/configuration.rb', line 22

def model_requests
  @model_requests
end

#models ⇒ Object

Returns the value of attribute models.



5
6
7
# File 'lib/rhino/configuration.rb', line 5

def models
  @models
end

#multi_tenant ⇒ Object

Returns the value of attribute multi_tenant.



5
6
7
# File 'lib/rhino/configuration.rb', line 5

def multi_tenant
  @multi_tenant
end

#nested ⇒ Object

Returns the value of attribute nested.



5
6
7
# File 'lib/rhino/configuration.rb', line 5

def nested
  @nested
end

#route_groups ⇒ Object

Returns the value of attribute route_groups.



5
6
7
# File 'lib/rhino/configuration.rb', line 5

def route_groups
  @route_groups
end

#route_key ⇒ Object

Global default route key: the column matched against the :id URL segment on member endpoints (show/update/destroy/restore/force_delete) for every model that does not declare its own rhino_route_key. Default nil = primary key (today's behavior, fully backward compatible).



11
12
13
# File 'lib/rhino/configuration.rb', line 11

def route_key
  @route_key
end

#test_framework ⇒ Object

Returns the value of attribute test_framework.



5
6
7
# File 'lib/rhino/configuration.rb', line 5

def test_framework
  @test_framework
end

Instance Method Details

#auth_enabled_groups ⇒ Object

Names of all groups (except :public) that opted into per-group auth.



242
243
244
245
# File 'lib/rhino/configuration.rb', line 242

def auth_enabled_groups
  @route_groups.keys.reject { |name| name.to_s == "public" }
               .select { |name| group_auth_enabled?(name) }
end

#auth_enabled_legacy_groups ⇒ Object

Names of auth-enabled groups that have an empty prefix AND no domain, i.e. groups whose auth routes would be byte-for-byte identical to the legacy unprefixed /api/auth/* set (GROUP_AUTH_DESIGN.md §11.1). Such a group IS the default/legacy auth: the legacy routes adopt its route_group/hooks instead of registering a colliding second set. Two or more is a conflict (raised by the route-group validator).



253
254
255
256
257
258
259
260
# File 'lib/rhino/configuration.rb', line 253

def auth_enabled_legacy_groups
  auth_enabled_groups.select do |name|
    group = @route_groups[name.to_sym]
    prefix = group[:prefix].to_s
    domain = group[:domain]
    prefix.empty? && (domain.nil? || domain.to_s.strip.empty?)
  end
end

#enforce_group_membership? ⇒ Boolean

Master flag (default off). When off, behavior is byte-for-byte today's: no group-membership enforcement.

Returns:

  • (Boolean)


65
66
67
# File 'lib/rhino/configuration.rb', line 65

def enforce_group_membership?
  !!@auth[:enforce_group_membership]
end

#group_auth_enabled?(group_name) ⇒ Boolean

Whether a group has per-group auth routes enabled (auth: true). The public group is never auth-enabled.

Returns:

  • (Boolean)


234
235
236
237
238
239
# File 'lib/rhino/configuration.rb', line 234

def group_auth_enabled?(group_name)
  return false if group_name.to_s == "public"

  group = @route_groups[group_name.to_sym]
  !!(group && group[:auth])
end

#group_is_tenant?(group_name) ⇒ Boolean

Whether a group is a tenant group (organization-scoped). Only the reserved :tenant group is treated as a tenant group, matching has_tenant_group?.

Returns:

  • (Boolean)


288
289
290
# File 'lib/rhino/configuration.rb', line 288

def group_is_tenant?(group_name)
  group_name.to_s == "tenant"
end

#group_tenant?(name) ⇒ Boolean

Whether the named route group has a tenant boundary, i.e. whether Rhino.query must fail closed inside it. Only a group that explicitly declares tenant: false does not; every other answer — an unknown group, an untagged route, or no group at all (jobs, rake tasks, console) — is true, so the resolver keeps failing closed wherever the group is not provably non-tenant.

Returns:

  • (Boolean)


162
163
164
165
166
167
168
169
# File 'lib/rhino/configuration.rb', line 162

def group_tenant?(name)
  return true if name.nil? || name.to_s.empty?

  group = @route_groups[name.to_sym]
  return true unless group

  group.fetch(:tenant, true) != false
end

#has_public_group? ⇒ Boolean

Whether a 'public' route group is configured

Returns:

  • (Boolean)


199
200
201
# File 'lib/rhino/configuration.rb', line 199

def has_public_group?
  @route_groups.key?(:public)
end

#has_tenant_group? ⇒ Boolean

Whether a 'tenant' route group is configured

Returns:

  • (Boolean)


194
195
196
# File 'lib/rhino/configuration.rb', line 194

def has_tenant_group?
  @route_groups.key?(:tenant)
end

#hooks_for_group(group_name) ⇒ Object

Resolve the configured lifecycle-hooks class for a group, instantiated. Returns nil when the group has no hooks configured. Accepts a class, a class name string, or an instance.



265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
# File 'lib/rhino/configuration.rb', line 265

def hooks_for_group(group_name)
  return nil if group_name.nil?

  group = @route_groups[group_name.to_sym]
  return nil unless group

  hooks = group[:hooks]
  return nil if hooks.nil?

  case hooks
  when String
    klass = hooks.safe_constantize
    klass&.new
  when Class
    hooks.new
  else
    hooks
  end
end

#model(slug, klass_name, store_request: nil, update_request: nil) ⇒ Object

Register a model with its slug Usage: config.model :posts, 'Post'

The optional store_request: / update_request: keywords override the {Model}StoreRequest / {Model}UpdateRequest naming convention for that model's POST / PUT action:

config.model :tasks, "Task", store_request: "CreateTask", update_request: "EditTask"

Class NAMES (strings) are stored, never constants, so a dev-mode Zeitwerk reload never hands back an unloaded class. A registration that cannot be constantized at request time raises Rhino::ConfigurationError rather than silently skipping validation.



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/rhino/configuration.rb', line 82

def model(slug, klass_name, store_request: nil, update_request: nil)
  key = slug.to_sym
  @models[key] = klass_name.to_s

  requests = {
    store: normalize_request_class_name(store_request),
    update: normalize_request_class_name(update_request)
  }

  if requests[:store].nil? && requests[:update].nil?
    @model_requests.delete(key)
  else
    @model_requests[key] = requests
  end
end

#model_in_group?(slug, group_name) ⇒ Boolean

Check if a specific slug belongs to a specific group

Returns:

  • (Boolean)


224
225
226
# File 'lib/rhino/configuration.rb', line 224

def model_in_group?(slug, group_name)
  models_for_group(group_name).include?(slug.to_sym)
end

#models_for_group(group_name) ⇒ Object

Resolve the model slugs for a given route group



204
205
206
207
208
209
210
211
212
213
214
# File 'lib/rhino/configuration.rb', line 204

def models_for_group(group_name)
  group = @route_groups[group_name.to_sym]
  return [] unless group

  group_models = group[:models]
  if group_models == :all || group_models == "*"
    @models.keys
  else
    Array(group_models).map(&:to_sym) & @models.keys
  end
end

#public_model?(slug) ⇒ Boolean

Check if a model belongs to the 'public' route group

Returns:

  • (Boolean)


217
218
219
220
221
# File 'lib/rhino/configuration.rb', line 217

def public_model?(slug)
  return false unless has_public_group?

  models_for_group(:public).include?(slug.to_sym)
end

#request_class_for(slug, action) ⇒ String?

The explicitly registered request class NAME for a model slug + action, or nil when the model relies on the naming convention.

Parameters:

  • slug (String, Symbol, nil)
  • action (String, Symbol) —

    "store" or "update"

Returns:

  • (String, nil)


104
105
106
107
108
109
110
111
# File 'lib/rhino/configuration.rb', line 104

def request_class_for(slug, action)
  return nil if slug.nil? || slug.to_s.empty?

  entry = @model_requests[slug.to_sym]
  return nil unless entry

  entry[action.to_s == "update" ? :update : :store]
end

#resolve_model(slug) ⇒ Object

Resolve a model class from its slug



172
173
174
175
176
177
178
179
180
181
182
# File 'lib/rhino/configuration.rb', line 172

def resolve_model(slug)
  klass_name = @models[slug.to_sym]
  raise ActiveRecord::RecordNotFound, "The #{slug} model does not exist" unless klass_name

  klass = klass_name.constantize
  raise ActiveRecord::RecordNotFound, "The #{slug} model does not exist" unless klass

  klass
rescue NameError
  raise ActiveRecord::RecordNotFound, "The #{slug} model does not exist"
end

#route_group(name, prefix: "", domain: nil, middleware: [], models: :all, auth: false, hooks: nil, tenant: true) ⇒ Object

Register a route group with its configuration Usage: config.route_group :tenant, prefix: ':organization', middleware: [Rhino::Middleware::ResolveOrganizationFromRoute], models: :all

The optional domain: keyword constrains the group's routes to a specific host. Two groups can then share the same prefix: but live on different domains. A parameterized domain such as "organization.example.com" captures the subdomain and feeds organization resolution exactly like the path-prefix ":organization" does. Groups without a domain (nil/blank) match any host (default, fully backward compatible).

The optional tenant: keyword declares whether the group has a tenant boundary. It defaults to true: Rhino.query inside the group fails closed, raising Rhino::MissingTenantContext when an organization-scopable model is queried with no organization resolved. Pass tenant: false for a group that legitimately spans every organization — a back office or admin group whose operators see all tenants' rows.



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
# File 'lib/rhino/configuration.rb', line 140

def route_group(name, prefix: "", domain: nil, middleware: [], models: :all, auth: false, hooks: nil,
                tenant: true)
  normalized_domain = domain.to_s.strip
  normalized_domain = nil if normalized_domain.empty?

  @route_groups[name.to_sym] = {
    prefix: prefix.to_s,
    domain: normalized_domain,
    middleware: Array(middleware),
    models: models,
    auth: !!auth,
    hooks: hooks,
    tenant: tenant != false
  }
end

#slug_for(model_class) ⇒ Object

Find the slug for a given model class



185
186
187
188
189
190
191
# File 'lib/rhino/configuration.rb', line 185

def slug_for(model_class)
  class_name = model_class.is_a?(Class) ? model_class.name : model_class.class.name
  @models.each do |slug, klass_name|
    return slug if klass_name == class_name
  end
  nil
end