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_sectionsrather 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
-
.defaults ⇒ Object
A fresh, mutable copy every call.
-
.enabled?(condition, view) ⇒ Boolean
The app-capability gate.
-
.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.
-
.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.
- .symbolize(hash) ⇒ Object
-
.warn_gate(message) ⇒ Object
Pure Ruby: this file loads without Rails, so it cannot assume a logger.
Class Method Details
.defaults ⇒ Object
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? }.
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.
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() if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger Rails.logger.warn() else Kernel.warn() end end |