Give FeaturePolicy a config object for env vars and user flags

On Virtualtrails I check features through a Pundit policy. Views call feature?(:staff_tools), feature?(:secret_site), or feature?(:native_bridge, context: app), and FeaturePolicy has a method that returns true or false.

The first version put the whole answer in that method:

def staff_tools?
  user&.admin?
end

Fine for “admins only”. Then I needed a boolean I could flip without a deploy, and a minimum app version so older native clients stay on the old UI. Those aren’t user checks. I didn’t want every feature?(:…) call to know whether it was reading env, a user record, or a version.

FeaturePolicy methods still return true or false. The enabled flags and minimum versions come from FeaturePolicy::Config. Defaults in the class are off. JSON in FEATURE_POLICY_CONFIG deep-merges over them.

class FeaturePolicy::Config
  ENV_KEY = "FEATURE_POLICY_CONFIG"

  DEFAULTS = {
    native_bridge: {
      minimum_version: { ios: nil, android: nil }
    },
    secret_site: {
      enabled: false
    }
  }.freeze

  def initialize
    @data = DEFAULTS.deep_dup.deep_merge(env_overrides)
  end

  def enabled?(feature)
    ActiveModel::Type::Boolean.new.cast(@data.dig(feature.to_sym, :enabled))
  end

  def minimum_version(feature, platform)
    return nil if platform.blank?

    coerce_minimum_version(@data.dig(feature.to_sym, :minimum_version, platform.to_sym))
  end
end

staff_tools? still looks at the user. secret_site? is the boolean. native_bridge? compares record.version (whatever you passed as context:) against the configured minimum:

def staff_tools?
  user&.admin?
end

def secret_site?
  self.class.config.enabled?(:secret_site)
end

def native_bridge?
  min = self.class.config.minimum_version(:native_bridge, record.platform)
  min.present? && record.version >= Gem::Version.new(min)
end

The env JSON looks like this:

{
  "native_bridge": {
    "minimum_version": {
      "ios": "21",
      "android": "15"
    }
  },
  "secret_site": {
    "enabled": true
  }
}

Unknown keys stay in the merged hash, so I can put a value in the environment before the code that reads it is deployed. Invalid JSON or a junk version string logs a warning and uses the defaults. A typo turns the feature off. It does not raise in a request.

If I add another source later, I’d merge it into @data next to the env JSON. Call sites keep saying feature?(:secret_site).