Module: LcpRuby::DynamicReferences::Resolver

Defined in:
lib/lcp_ruby/dynamic_references/resolver.rb

Overview

Single source of truth for resolving “magic-token” strings used in YAML/DSL metadata: record-rule scope ‘value:`, page `scope_context:`, condition `value:`, workflow `set_fields:`, and field `default:`.

Grammar (string-only, dotted notation):

current_user                  → context[:user]
current_user.<method>         → context[:user].send(method) if respond_to?
record                        → context[:record]
record.<field>                → context[:record].send(field) if respond_to?
current_date                  → Date.current
current_datetime              → Time.current
current_year                  → Date.current.year
selection_id                  → context[:selection_id]

Runtime contract:

* Missing context object (nil user / nil record) → returns nil silently.
* Method/field present in token but missing on a non-nil object →
  dev/test raises; production records + returns nil
  (per the canonical 3-line idiom in `lib/lcp_ruby.rb#record_error`).
* Unknown root or shape → ParseError (programmer error — caller's bug).

Use ‘Validator.validate_token` at boot-time to catch typos in `current_user.<method>` against the configured user_class.

Defined Under Namespace

Classes: ParseError, ResolutionError

Constant Summary collapse

MAX_DOT_DEPTH =
1
ROOT_TOKENS =
%w[
  current_user
  record
  current_date
  current_datetime
  current_year
  selection_id
].freeze
DOTTABLE_ROOTS =

Roots that take an optional .<method> suffix.

%w[current_user record].freeze

Class Method Summary collapse

Class Method Details

.parse(token) ⇒ Hash

Pure parser — no context, no side effects. Used by Validator.

Parameters:

  • token (String)

Returns:

  • (Hash)

    { root: String, method: String|nil }

Raises:



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/lcp_ruby/dynamic_references/resolver.rb', line 66

def parse(token)
  unless token.is_a?(String) && !token.empty?
    raise ParseError, "dynamic reference must be a non-empty String, got #{token.inspect}"
  end

  # split(".", -1) preserves trailing empty segments so "current_user."
  # parses as ["current_user", ""] and falls into the invalid-method branch.
  segments = token.split(".", -1)
  root = segments.first
  unless ROOT_TOKENS.include?(root)
    raise ParseError,
      "unknown dynamic reference root '#{root}' in '#{token}' " \
      "(expected one of: #{ROOT_TOKENS.join(', ')})"
  end

  if segments.size == 1
    { root: root, method: nil }
  elsif segments.size == 2
    unless DOTTABLE_ROOTS.include?(root)
      raise ParseError,
        "dynamic reference '#{token}' is not allowed: '#{root}' does not take a .<method> suffix"
    end
    method = segments[1]
    if method.empty? || !method.match?(/\A[a-z_][a-z0-9_]*\z/)
      raise ParseError, "invalid method name '#{method}' in dynamic reference '#{token}'"
    end
    { root: root, method: method }
  else
    raise ParseError,
      "dynamic reference '#{token}' exceeds maximum dot-depth of #{MAX_DOT_DEPTH}"
  end
end

.recognized?(value) ⇒ Boolean

Returns true iff the value is a recognized dynamic-reference string. Useful for callers that mix literals and dynamic refs in the same slot.

Returns:

  • (Boolean)


101
102
103
104
105
106
107
# File 'lib/lcp_ruby/dynamic_references/resolver.rb', line 101

def recognized?(value)
  return false unless value.is_a?(String)
  parse(value)
  true
rescue ParseError
  false
end

.resolve(token, context: {}) ⇒ Object?

Resolves a string token to its runtime value.

Parameters:

  • token (String)

    dynamic reference (e.g. “current_user.id”, “record.title”)

  • context (Hash) (defaults to: {})

    { user:, record:, selection_id: } — all optional

Returns:

  • (Object, nil)

    resolved value or nil

Raises:

  • (ParseError)

    if the token shape is invalid (bad root, depth > 1)

  • (ResolutionError)

    in dev/test if a method is missing on a known object



56
57
58
59
# File 'lib/lcp_ruby/dynamic_references/resolver.rb', line 56

def resolve(token, context: {})
  parsed = parse(token)
  dispatch(parsed, context)
end