Module: Crystalline::MetadataFields::ClassMethods

Defined in:
lib/crystalline/metadata_fields.rb

Constant Summary collapse

UNRESOLVED_UNION =

Sentinel returned when a union value cannot be resolved to any member type, so the caller can leave the field unset (instead of assigning nil).

Object.new

Instance Method Summary collapse

Instance Method Details

#field(field_name, type, metadata = {}) ⇒ Object



27
28
29
30
31
# File 'lib/crystalline/metadata_fields.rb', line 27

def field(field_name, type,  = {})
  attr_accessor field_name

  fields << Field.new(field_name, type, )
end

#field_augmented?Boolean

Returns:



33
34
35
# File 'lib/crystalline/metadata_fields.rb', line 33

def field_augmented?
  true
end

#fieldsObject



21
22
23
24
25
# File 'lib/crystalline/metadata_fields.rb', line 21

def fields
  @__fields__ = [] if @__fields__.nil?

  @__fields__
end

#from_dict(d) ⇒ Object



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
# File 'lib/crystalline/metadata_fields.rb', line 66

def from_dict(d)
  to_build = {}

  fields.each do |field|
    key = field.name
     = field..fetch(:format_json, {})
    lookup = .fetch(:letter_case, nil).call
    value = d[lookup]
    field_type = field.type
    if ::Crystalline::Utils.nilable? field_type
      if value == 'null'
        to_build[key] = nil
        next
      end
      field_type = ::Crystalline::Utils.nilable_of(field_type)
    end
    
    # If field is not nilable, and the value is not in the dict, raise a KeyError
    raise KeyError, "key #{lookup} not found in hash" if value.nil? && !::Crystalline::Utils.nilable?(field.type)
    # If field is nilable, and the value is not in the dict, just move to the next field
    next if value.nil?

    result = unmarshal_field_value(field, field_type, value, )
    to_build[key] = result unless result.equal?(UNRESOLVED_UNION)
  end
  new(**to_build)
end

#unmarshal_field_value(field, field_type, value, format_metadata) ⇒ Object

Dispatches unmarshalling of a single field value based on its declared type.



99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/crystalline/metadata_fields.rb', line 99

def unmarshal_field_value(field, field_type, value, )
  if Crystalline::Utils.arr? field_type
    inner_type = Crystalline::Utils.arr_of(field_type)
    value.map { |f| unmarshal_single(inner_type, f, ) }
  elsif Crystalline::Utils.hash? field_type
    val_type = Crystalline::Utils.hash_of(field_type)
    # rubocop:disable Style/HashTransformValues
    value.map { |k, v| [k, unmarshal_single(val_type, v, )] }.to_h
    # rubocop:enable Style/HashTransformValues
  elsif Crystalline::Utils.union? field_type
    unmarshal_union(field, field_type, value)
  elsif field_type.instance_of?(Class) && field_type.include?(::Crystalline::MetadataFields)
    Crystalline.unmarshal_json(value, field_type)
  else
    unmarshal_single(field_type, value, )
  end
end

#unmarshal_single(field_type, value, format_metadata = nil) ⇒ Object



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/crystalline/metadata_fields.rb', line 37

def unmarshal_single(field_type, value,  = nil)
  decoder = .fetch(:decoder, nil)

  if field_type.instance_of?(Class) && field_type.include?(::Crystalline::MetadataFields)
    return field_type.from_dict(value)
  elsif field_type.to_s == 'Date'
    return Date.parse(value)
  elsif field_type.to_s == 'DateTime'
    return DateTime.parse(value)
  elsif field_type.to_s == 'Object'
    # rubocop:disable Lint/SuppressedException
    begin
      value = JSON.parse(value)
    rescue TypeError, JSON::ParserError
      # Not valid JSON; keep the original value unchanged.
    end
    # rubocop:enable Lint/SuppressedException
    return value
  elsif field_type.to_s == 'Float'
    return value.to_f

  end
  if decoder.nil?
    value
  else
    decoder.call(value)
  end
end

#unmarshal_union(field, field_type, value) ⇒ Object

Resolves a union-typed value, using a discriminator when present and otherwise trying each candidate type in turn. Returns UNRESOLVED_UNION if none match.



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/crystalline/metadata_fields.rb', line 119

def unmarshal_union(field, field_type, value)
  discriminator = field..fetch(:discriminator, nil)
  unless discriminator.nil?
    discriminator_mapping = field..fetch(:discriminator_mapping, nil)
    discriminator_value = value.fetch(discriminator)
    type_to_deserialize =
      if discriminator_mapping.nil?
        # Fallback: try to match discriminator value against type name
        Crystalline::Utils.get_union_types(field_type).find { |t| t.name.split('::').last == discriminator_value }
      else
        # Use explicit mapping from discriminator value to type
        discriminator_mapping[discriminator_value]
      end
    return Crystalline.unmarshal_json(value, type_to_deserialize)
  end

  union_types = Crystalline::Utils.get_union_types(field_type)
  union_types = union_types.sort_by { |klass| Crystalline.non_nilable_attr_count(klass) }
  union_types.each do |union_type|
    return Crystalline.unmarshal_json(value, union_type)
  rescue TypeError, NoMethodError, KeyError
    next
  end
  UNRESOLVED_UNION
end