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 },
  # Gated on the app OFFERING Google, not merely on having the columns —
  # the same question app/views/sessions/new.html.erb:79 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.



79
80
81
# File 'lib/studio/profile_sections.rb', line 79

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)


106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/studio/profile_sections.rb', line 106

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)
    return false unless view.respond_to?(condition)

    return !!view.public_send(condition)
  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.



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

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)


132
133
134
135
136
137
138
# File 'lib/studio/profile_sections.rb', line 132

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



140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/studio/profile_sections.rb', line 140

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