Module: Cline::Utils::Schema

Defined in:
lib/cline/utils/schema.rb

Overview

Provide some utility methods that are related to schema handling

Internal collapse

Class Method Details

.collection(shale_type) ⇒ Class

Provide a class that can be used by Shale to read and write arrays of objects, like this: [ { ... another Shale structure ... }, { ... another Shale structure ... }, ... { ... another Shale structure ... } ] We don't use the native Shale collection's feature as it does not support type casting at element, and so prevents initialization like that: my_object.my_collection = [{ attr: 1 }, { attr: 2 }].

Parameters:

  • shale_type (Symbol, Class)

    The shale type that this class should expect for the array's values

Returns:

  • (Class)

    Class that can be used in an attribute declaration



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/cline/utils/schema.rb', line 164

def self.collection(shale_type)
  shale_type = Shale::Type.lookup(shale_type) if shale_type.is_a?(Symbol)
  # Give a name to this class because Shale needs it to reference it in its accessors.
  # That also allows us to not redefine the same class several times.
  external_name = :"CollectionOf#{shale_type.to_s.gsub(':', '')}"
  unless Schema.const_defined?(external_name)
    schema_class = Class.new(Cline::Schema) do
      extend Forwardable

      # @!group Public API

      include Enumerable

      def_delegators :elements, *%i[[] each empty? last size]

      # Constructor
      #
      # @param elements [Array<shale_type>] The elements to initialize the structure with
      def initialize(elements = [])
        super()
        @elements = elements
      end

      # Set a value at a given index.
      # Handle potential casting for the value.
      #
      # @param idx [Integer] The integer to set.
      # @param value [Object] The value to set.
      def []=(idx, value)
        elements[idx] = self.class.value_type.cast(value)
      end

      # Append a value.
      # Handle potential casting for the value.
      #
      # @param value [Object] The value to set.
      def <<(value)
        elements << self.class.value_type.cast(value)
      end

      # Equality check
      #
      # @param other [Object] The other to check equality with
      # @return [Boolean] True if objects are equal
      def ==(other)
        other.is_a?(self.class) &&
          other.elements == elements
      end

      # @!group Internal

      # Output this object as a Ruby object that can be JSON-serialized.
      #
      # @return [Object] Ruby object ready for JSON
      def to_hash
        elements.map { |element| element.respond_to?(:to_hash) ? element.to_hash : element }
      end

      # @return [Hash] The elements array
      attr_accessor :elements

      class << self
        attr_accessor :value_type, :external_name

        # Hook called when a subclass inherits our class
        #
        # @param subclass [Class] The inheriting class
        def inherited(subclass)
          super
          subclass.value_type = value_type
        end

        # Parse a Ruby object and instantiate the proper instance from it.
        #
        # @param hash [Object] Data
        # @param args [Array] Remaining arguments to be transferred to Shale
        # @param kwargs [Hash] Remaining kwargs to be transferred to Shale
        # @return [Schema] Corresponding instance
        def of_hash(hash, *args, **kwargs)
          new(hash.map { |element| value_type.of_hash(element, *args, **kwargs) })
        end

        # Get a Ruby object from an instance.
        #
        # @param instance [Schema] Object to serialize to a Hash
        # @param args [Array] Remaining arguments to be transferred to Shale
        # @param kwargs [Hash] Remaining kwargs to be transferred to Shale
        # @return [Object] Corresponding Ruby object
        def as_hash(instance, *args, **kwargs)
          instance.elements.map { |element| value_type.as_hash(element, *args, **kwargs) }
        end

        # Cast an input value to this Schema object.
        # Allow the attribute to be initialized directly using its Array form.
        #
        # @param value [Schema, Array, nil] The value that could be used to initialize a new instance of this attribute.
        # @return [Schema, nil] The corresponding instance, or nil if none.
        def cast(value)
          return nil if value.nil?

          # We expect the value to be a Array of values that can themselves be cast using the Shale type, or a new instance already initialized.
          if value.is_a?(self)
            value
          elsif value.is_a?(Array)
            new(value.map { |element| value_type.cast(element) })
          else
            raise "Unable to cast #{value} into #{name}"
          end
        end

        # Return the class name
        #
        # @return [String] The class name
        def to_s
          "::Cline::Utils::Schema::#{@external_name}"
        end
      end
    end
    schema_class.value_type = shale_type
    schema_class.external_name = external_name
    Schema.const_set(external_name, schema_class)
  end
  Schema.const_get(external_name)
end

.map(shale_type) ⇒ Class

Provide a class that can be used by Shale to read and write maps of objects (dynamic keys), like this: { "key1": { ... another Shale structure ... }, "key2": { ... another Shale structure ... }, ... "keyN": { ... another Shale structure ... } }

Parameters:

  • shale_type (Symbol, Class)

    The shale type that this class should expect for the map's values

Returns:

  • (Class)

    Class that can be used in an attribute declaration



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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
65
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
98
99
100
101
102
103
104
105
106
107
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
142
143
144
145
146
147
148
149
150
# File 'lib/cline/utils/schema.rb', line 20

def self.map(shale_type)
  shale_type = Shale::Type.lookup(shale_type) if shale_type.is_a?(Symbol)
  # Give a name to this class because Shale needs it to reference it in its accessors.
  # That also allows us to not redefine the same class several times.
  external_name = :"MapOf#{shale_type.to_s.gsub(':', '')}"
  unless Schema.const_defined?(external_name)
    schema_class = Class.new(Cline::Schema) do
      extend Forwardable

      # @!group Public API

      include Enumerable

      def_delegators :elements_hash, *%i[[] each empty? key? keys size to_hash values]

      # Constructor
      #
      # @param elements_hash [Hash{String => shale_type}] The elements to initialize the structure with
      def initialize(elements_hash = {})
        super()
        @elements_hash = elements_hash
      end

      # Set a key and its corresponding value.
      # Handle potential casting for the value.
      #
      # @param key [Object] The key to set.
      # @param value [Object] The value to set.
      def []=(key, value)
        elements_hash[key] = self.class.value_type.cast(value)
      end

      # Equality check
      #
      # @param other [Object] The other to check equality with
      # @return [Boolean] True if objects are equal
      def ==(other)
        other.is_a?(self.class) &&
          other.elements_hash == elements_hash
      end

      # @!group Internal

      # @return [Hash] The elements hash taken from the extra attributes
      attr_accessor :elements_hash

      class << self
        attr_accessor :value_type, :external_name

        # Hook called when a subclass inherits our class
        #
        # @param subclass [Class] The inheriting class
        def inherited(subclass)
          super
          subclass.value_type = value_type
        end

        # Parse a Hash object and instantiate the proper instance from it.
        #
        # @param hash [Hash] Data
        # @param args [Array] Remaining arguments to be transferred to Shale
        # @param kwargs [Hash] Remaining kwargs to be transferred to Shale
        # @return [Schema] Corresponding instance
        def of_hash(hash, *args, **kwargs)
          instance = super
          # Move the extra attributes into properly constructed elements_hash
          instance.elements_hash =
            if instance.extra_attributes.nil?
              {}
            else
              instance.extra_attributes.to_h do |key, value|
                [
                  key,
                  @value_type.of_hash(value, *args, **kwargs)
                ]
              end
            end
          instance
        end

        # Get a Hash object from an instance.
        #
        # @param instance [Schema] Object to serialize to a Hash
        # @param args [Array] Remaining arguments to be transferred to Shale
        # @param kwargs [Hash] Remaining kwargs to be transferred to Shale
        # @return [Hash] Corresponding hash
        def as_hash(instance, *args, **kwargs)
          # Set the extra_attributes properly
          unless instance.elements_hash.empty?
            instance.extra_attributes = instance.elements_hash.to_h do |key, value|
              [
                key,
                @value_type.as_hash(value, *args, **kwargs)
              ]
            end
          end
          super
        end

        # Cast an input value to this Schema object.
        # Allow the attribute to be initialized directly using its Hash form.
        #
        # @param value [Schema, Hash, nil] The value that could be used to initialize a new instance of this attribute.
        # @return [Schema, nil] The corresponding instance, or nil if none.
        def cast(value)
          return nil if value.nil?

          # We expect the value to be a Hash of values that can themselves be cast using the Shale type, or a new instance already initialized.
          if value.is_a?(self)
            value
          elsif value.is_a?(Hash)
            new(value.to_h { |k, v| [k, value_type.cast(v)] })
          else
            raise "Unable to cast #{value} into #{name}"
          end
        end

        # Return the class name
        #
        # @return [String] The class name
        def to_s
          "::Cline::Utils::Schema::#{@external_name}"
        end
      end
    end
    schema_class.value_type = shale_type
    schema_class.external_name = external_name
    Schema.const_set(external_name, schema_class)
  end
  Schema.const_get(external_name)
end