Module: Studio::ProfileSections

Defined in:
lib/studio/profile_sections.rb

Constant Summary collapse

DEFAULTS =

The page every consumer gets for free. Iteration one is deliberately two rows: the picture and the name. Everything else the account-standardization program lifts (email change, identities, preferences) arrives as further defaults, and a host that has declared its own list keeps working because it composes against Studio.default_profile_sections rather than a literal.

[
  { key: :avatar, title: "Profile photo",
    partial: "studio/profiles/avatar_section", requires: :avatar, modals: true },
  { key: :first_name, title: "Your name",
    partial: "studio/profiles/first_name_section", requires: :first_name },
  { key: :email, title: "Email",
    partial: "studio/profiles/email_section", requires: :email },
  # Gated on the app OFFERING Google, not merely on having the columns —
  # the same question app/views/sessions/new.html.erb already asks before
  # drawing this identical button on the login page.
  { key: :google, title: "Google account",
    partial: "studio/profiles/google_section", requires: %i[provider uid],
    if: -> { Studio.auth_method?(:google) } }
].freeze

Class Method Summary collapse

Class Method Details

.defaultsObject

A fresh, mutable copy every call. Hosts compose with + and sometimes reject, and handing out the frozen literal would let one host's edit leak into the next request's page.



81
82
83
# File 'lib/studio/profile_sections.rb', line 81

def defaults
  DEFAULTS.map { |section| section.dup }
end

.enabled?(condition, view) ⇒ Boolean

The app-capability gate. No if: means always enabled. The callable takes the view when it wants one and nothing when it does not, so a host can write either -> { Studio.feature?(:x) } or ->(view) { view.admin? }.

Returns:

  • (Boolean)


108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/studio/profile_sections.rb', line 108

def enabled?(condition, view)
  return true if condition.nil?

  # A Symbol/String names a method on the VIEW — Rails' own
  # `before_action ..., if: :method_name` convention, and therefore the most
  # natural thing a host will write here.
  #
  # It is handled explicitly because the alternative FAILS OPEN: a Symbol
  # does not answer `call`, so the earlier `return !!condition` coerced
  # `:some_predicate` to true and rendered the row unconditionally — a gate
  # that silently does nothing, in the same permissive direction as the bug
  # this whole `if:` key was added to fix. A host would have had no signal.
  if condition.is_a?(Symbol) || condition.is_a?(String)
    unless view.respond_to?(condition)
      # SILENT would repeat the bug this key was added to fix. The old
      # coercion gave the host no signal; dropping the row without one only
      # moves the silence somewhere safer. A typo'd gate now says so.
      warn_gate("profile_sections: `if: #{condition.inspect}` names a method the view " \
                "does not answer — the row was dropped. Check the spelling.")
      return false
    end

    # Arity-aware, matching the lambda branch below: a predicate written as
    # `def visible?(view)` is as natural as one written without an argument,
    # and calling it wrong raises ArgumentError — which surfaces as a 500 on
    # /profile rather than as a dropped row.
    method = view.method(condition)
    return !!(method.arity.zero? ? view.public_send(condition) : view.public_send(condition, view))
  end

  return !!condition unless condition.respond_to?(:call)

  !!(condition.arity.zero? ? condition.call : condition.call(view))
end

.resolve(declared, view) ⇒ Object

Declared may be nil (the host has said nothing — it gets the defaults), an Array, or a callable receiving the view context.

nil meaning "the defaults" rather than "no sections" is the load-bearing choice here: it is what makes a brand-new app's profile page work with an empty initializer, which is the entire point of standardizing this.



91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/studio/profile_sections.rb', line 91

def resolve(declared, view)
  sections = declared.respond_to?(:call) ? declared.call(view) : declared
  sections = defaults if sections.nil?

  admin = view.respond_to?(:admin?) && view.admin?
  user  = view.respond_to?(:current_user) ? view.current_user : nil

  Array(sections)
    .map { |section| symbolize(section) }
    .reject { |section| section[:admin] && !admin }
    .select { |section| enabled?(section[:if], view) }
    .select { |section| served_by?(user, section[:requires]) }
end

.served_by?(user, requires) ⇒ Boolean

Can this host's user model serve the row? No requirement means yes — a row that reads nothing off the user (a static explainer, a link out) is always served. A nil user means we are rendering for nobody, and nothing is served.

Returns:

  • (Boolean)


146
147
148
149
150
151
152
# File 'lib/studio/profile_sections.rb', line 146

def served_by?(user, requires)
  needed = Array(requires).compact
  return true if needed.empty?
  return false if user.nil?

  needed.all? { |attribute| user.respond_to?(attribute) }
end

.symbolize(hash) ⇒ Object



163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/studio/profile_sections.rb', line 163

def symbolize(hash)
  out = hash.to_h.each_with_object({}) { |(key, value), acc| acc[key.to_sym] = value }

  # `key` is an IDENTIFIER, so its VALUE symbolizes too — unlike every other
  # field, whose value is data. A host composing by key writes
  # `reject { |s| s[:key] == :avatar }`, and that silently matches nothing if
  # a section declared with string keys kept `"avatar"` as a String. Removing
  # a standard row is the documented seam; it must not depend on which
  # spelling the host happened to use.
  out[:key] = out[:key].to_sym if out[:key].respond_to?(:to_sym)
  out
end

.warn_gate(message) ⇒ Object

Pure Ruby: this file loads without Rails, so it cannot assume a logger.



155
156
157
158
159
160
161
# File 'lib/studio/profile_sections.rb', line 155

def warn_gate(message)
  if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
    Rails.logger.warn(message)
  else
    Kernel.warn(message)
  end
end