cline-rb 2.1.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a01259d29dbebf9ef8f098c9d86f4542719c9e7721ba6f8a31458ace1748216c
4
- data.tar.gz: 55905c7acc0e83133241e54d68242d5a2a59a9aa212885eea06c485094a66c57
3
+ metadata.gz: 5df017c7913b29844cc5cdc1611e355bcee832d6f75c09d0765a88eb9ff91b3a
4
+ data.tar.gz: 355b9674891ed24101c3f44d5087edf7060f33f85a8a4b010aedca55014d03b5
5
5
  SHA512:
6
- metadata.gz: 7471d4867c48f307b11fe7218db73ff8e1cdd61e1510a18db21bc2aedfe4fcb55f5632971d9ecc5b86c2b6426fd506488c9255e6abcb7b977deff584ddf39f23
7
- data.tar.gz: fe6f2921cfa9128d7aafb96d1f3c82ef06dde9eba649097d174bc41e93911dcd0910e3b5c95f35c27a7b0e5da4468193b237510b5ba2e8b34feff93226226541
6
+ metadata.gz: 918c2580895ea328088cf37bd92214c510417a7c457d6fc12e2826a807eaedb6800ae1b24c87064c7103defa61fe9d8ca042f1e8aae0ff0a2aad9bf680167c4e
7
+ data.tar.gz: bd0abcf30c86ed77861df47ab3c46d6ccee9348ff17b5662c431ae9c515f69478f863367e17baf7a12f63535585f246d975c1d87f55cf97e9c5aed0f000ae17f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,32 @@
1
+ # [v2.3.0](https://github.com/Muriel-Salvan/cline-rb/compare/v2.2.0...v2.3.0) (2026-11-04 15:21:27)
2
+
3
+ ## Global changes
4
+ ### Patches
5
+
6
+ * [feat(cline): add LenientSchema for tolerant deserialization of session messages's input](https://github.com/Muriel-Salvan/cline-rb/commit/a033d62989c22876030742c5e4979dc7a02bee16)
7
+
8
+ ## Changes for cline
9
+ ### Features
10
+
11
+ * [feat(cline): add LenientSchema for tolerant deserialization of session messages's input](https://github.com/Muriel-Salvan/cline-rb/commit/a033d62989c22876030742c5e4979dc7a02bee16)
12
+
13
+ ## Changes for schema
14
+ ### Features
15
+
16
+ * [feat(cline): add LenientSchema for tolerant deserialization of session messages's input](https://github.com/Muriel-Salvan/cline-rb/commit/a033d62989c22876030742c5e4979dc7a02bee16)
17
+
18
+ # [v2.2.0](https://github.com/Muriel-Salvan/cline-rb/compare/v2.1.1...v2.2.0) (2026-11-03 16:44:07)
19
+
20
+ ## Global changes
21
+ ### Patches
22
+
23
+ * [feat(session-message): add tool input attributes and origin metadata](https://github.com/Muriel-Salvan/cline-rb/commit/b873024a5d85f1032f63af03742e713d84f0818f)
24
+
25
+ ## Changes for session-message
26
+ ### Features
27
+
28
+ * [feat(session-message): add tool input attributes and origin metadata](https://github.com/Muriel-Salvan/cline-rb/commit/b873024a5d85f1032f63af03742e713d84f0818f)
29
+
1
30
  # [v2.1.1](https://github.com/Muriel-Salvan/cline-rb/compare/v2.1.0...v2.1.1) (2026-10-28 09:57:49)
2
31
 
3
32
  ## Global changes
data/README.md CHANGED
@@ -592,7 +592,7 @@ msg.usage #=> Usage struct (cost, tokens)
592
592
  msg.to_human #=> human-friendly string (limited to 128 chars by default)
593
593
  ```
594
594
 
595
- Contains nested schemas: `ModelInfo`, `Metrics`, `MessageContent`, `ToolUseInput`.
595
+ Contains nested schemas: `ModelInfo`, `Metrics`, `MessageContent`, `ToolUseInput`, `ToolResultContent`.
596
596
 
597
597
  ---
598
598
 
@@ -877,6 +877,7 @@ Every domain object (settings, providers, sessions, tasks...) extends `Cline::Sc
877
877
  - **Declarative attributes** — `attribute :my_field, :string` mirrors the JSON schema.
878
878
  - **Automatic camelCase↔snake_case mapping** — Cline stores `autoUpdateEnabled`, Ruby gets `auto_update_enabled`.
879
879
  - **Extra attribute preservation** — unknown JSON keys are stored in `extra_attributes` and re-serialized back, maintaining round-trip fidelity.
880
+ - **Typed containers & unions** — `Utils::Schema.collection(Type)` models arrays, `Utils::Schema.map(:string)` dynamic-key maps and `Utils::Schema.union(:string, Utils::Schema.collection(Type))` attributes that can be of several types.
880
881
 
881
882
  3 mixin modules handle file persistence — classes include them to "come alive" from disk:
882
883
 
data/lib/cline/cli.rb CHANGED
@@ -116,7 +116,7 @@ module Cline
116
116
  # or nil if none (see Session#monitor_message)
117
117
  # @param on_question [#call, nil] Callback called for each question that is asked by the assistant, or nil if none.
118
118
  # This should be set if an interactive session is expected.
119
- # If a question is asked without a callback to handle it, an UnexpectedInteractiveSessionError exceptioon will be raised.
119
+ # If a question is asked without a callback to handle it, an UnexpectedInteractiveSessionError exception will be raised.
120
120
  # - Param question [SessionMessage::MessageContent::ToolUseInput] Question input with possible options
121
121
  # (see SessionMessage::MessageContent::ToolUseInput).
122
122
  # - Return [String] The answer that the user should provide to this assistant's question.
@@ -0,0 +1,116 @@
1
+ module Cline
2
+ # Base class for schemas whose data may not respect the expected types on deserialization.
3
+ # This is typically the case for attributes filled by an LLM, whose types are not guaranteed
4
+ # by the Cline CLI (for example a String given where an Array is expected).
5
+ #
6
+ # Contrary to {Schema}, deserialization does not raise exceptions for such wrong data:
7
+ # a warning is emitted, every attribute that can be deserialized properly is set on the instance,
8
+ # and the other attributes are stored in {#wrong_attributes}, keyed by their attribute name.
9
+ #
10
+ # @example
11
+ # input = Cline::SessionMessage::MessageContent::ToolUseInput.of_hash(
12
+ # 'question' => 'Continue?',
13
+ # 'files' => 'not-an-array'
14
+ # )
15
+ # input.question #=> 'Continue?'
16
+ # input.files #=> nil
17
+ # input.wrong_attributes #=> { files: 'not-an-array' }
18
+ class LenientSchema < Schema
19
+ extend Utils::Logger
20
+
21
+ # @!group Public API
22
+
23
+ # The attributes that could not be deserialized, keyed by their attribute name, with their raw value.
24
+ # Only set when at least one attribute could not be deserialized.
25
+ # When the whole value is not a Hash, it is stored under the '_raw' key.
26
+ #
27
+ # @return [Hash{Symbol => Object}, nil] The wrong attributes, or nil if deserialization succeeded
28
+ attr_accessor :wrong_attributes
29
+
30
+ class << self
31
+ # @!group Internal
32
+
33
+ # Cast an input value to this Schema object.
34
+ # Any value that cannot be cast is turned into an empty instance with {#wrong_attributes} set,
35
+ # instead of raising an exception.
36
+ #
37
+ # @param value [Schema, Hash, nil] The value that could be used to initialize a new instance of this attribute
38
+ # @return [Schema, nil] The corresponding instance, or nil if none
39
+ def cast(value)
40
+ super
41
+ rescue StandardError => e
42
+ # TODO: Make this a warn message when logger will be implemented properly
43
+ log_debug "!!! Cline: Unable to cast a #{value.class} into a #{name} (#{e.class}: #{e.message}). Raw value: #{value.to_s[0, 256]}"
44
+ instance = new
45
+ instance.wrong_attributes = { '_raw' => value.to_s }
46
+ instance
47
+ end
48
+
49
+ # Parse a Hash object and instantiate the proper instance from it.
50
+ # Instead of raising an exception when an attribute cannot be deserialized
51
+ # (wrong type, wrong structure...), this attribute is left unset and its raw value
52
+ # is stored in {#wrong_attributes}, while a warning is emitted.
53
+ # Attributes that can be deserialized properly are still set on the instance.
54
+ #
55
+ # @param hash [Hash] Data
56
+ # @param args [Array] Remaining arguments to be transferred to Shale
57
+ # @param kwargs [Hash] Remaining kwargs to be transferred to Shale
58
+ # @return [LenientSchema] Corresponding instance
59
+ def of_hash(hash, *args, **kwargs)
60
+ complete_hash_mapping
61
+ instance = new
62
+ unless hash.is_a?(Hash)
63
+ # TODO: Make this a warn message when logger will be implemented properly
64
+ log_debug "Cline: Unable to deserialize a #{name} from a #{hash.class}. Raw value: #{hash.to_s[0, 256]}"
65
+ instance.wrong_attributes = { '_raw' => hash.to_s }
66
+ return instance
67
+ end
68
+ mappings = hash_mapping.keys
69
+ wrong_attributes = {}
70
+ hash.each do |key, value|
71
+ mapping = mappings[key]
72
+ if mapping.nil?
73
+ # Keep unknown attributes for round-trip fidelity, as Schema does
74
+ (instance.extra_attributes ||= {})[key] = value
75
+ next
76
+ end
77
+ begin
78
+ set_attribute_from_hash(instance, mapping, value, *args, **kwargs)
79
+ rescue StandardError => e
80
+ # TODO: Make this a warn message when logger will be implemented properly
81
+ log_debug "Cline: Unable to deserialize attribute #{key.inspect} of #{name} (#{e.class}: #{e.message}). Raw value: #{value.to_s[0, 256]}"
82
+ wrong_attributes[mapping.attribute] = value
83
+ end
84
+ end
85
+ instance.wrong_attributes = wrong_attributes unless wrong_attributes.empty?
86
+ instance
87
+ end
88
+
89
+ private
90
+
91
+ # Set one attribute of an instance from its Hash value, mirroring Shale's own of_hash logic.
92
+ #
93
+ # @param instance [LenientSchema] The instance to set the attribute on
94
+ # @param mapping [Shale::Mapping::Descriptor::Dict] The hash mapping of the attribute
95
+ # @param value [Object] The raw value to deserialize
96
+ # @param args [Array] Remaining arguments to be transferred to Shale
97
+ # @param kwargs [Hash] Remaining kwargs to be transferred to Shale
98
+ # @return [void]
99
+ def set_attribute_from_hash(instance, mapping, value, *args, **kwargs)
100
+ attribute = attributes[mapping.attribute]
101
+ check_scalar_value!(attribute, value)
102
+ casted_value =
103
+ if value.nil?
104
+ nil
105
+ elsif attribute.collection?
106
+ (value.is_a?(Array) ? value : [value]).map do |element|
107
+ element.nil? ? nil : attribute.type.cast(attribute.type.of_hash(element, *args, **kwargs))
108
+ end
109
+ else
110
+ attribute.type.cast(attribute.type.of_hash(value, *args, **kwargs))
111
+ end
112
+ instance.send(attribute.setter, casted_value)
113
+ end
114
+ end
115
+ end
116
+ end
data/lib/cline/schema.rb CHANGED
@@ -10,6 +10,8 @@ module Cline
10
10
  class Schema < Shale::Mapper
11
11
  # @!group Internal
12
12
 
13
+ include Utils::Logger
14
+
13
15
  # @return [Hash] Store all extra values from a JSON parse
14
16
  attr_accessor :extra_attributes
15
17
 
@@ -23,6 +25,31 @@ module Cline
23
25
  @snake_attributes.uniq!
24
26
  end
25
27
 
28
+ # Enable strict type checks on deserialization.
29
+ # When enabled, a scalar attribute that receives an Array or a Hash raises a TypeError
30
+ # instead of being silently coerced.
31
+ #
32
+ # @return [void]
33
+ def strict_types!
34
+ @strict_types = true
35
+ end
36
+
37
+ # Are strict type checks enabled on deserialization?
38
+ #
39
+ # @return [Boolean] true if strict type checks are enabled
40
+ def strict_types?
41
+ @strict_types == true
42
+ end
43
+
44
+ # Check if a raw value can be deserialized by this schema.
45
+ # By default a schema is deserialized from a Hash, or from an instance of itself.
46
+ #
47
+ # @param raw_value [Object] The raw value to check
48
+ # @return [Boolean] true if {of_hash} can deserialize this value
49
+ def accepts_value?(raw_value)
50
+ raw_value.is_a?(self) || raw_value.is_a?(Hash)
51
+ end
52
+
26
53
  # Parse a Hash object and instantiate the proper instance from it.
27
54
  #
28
55
  # @param hash [Hash] Data
@@ -42,6 +69,7 @@ module Cline
42
69
  extra_hash[key] = value
43
70
  end
44
71
  end
72
+ validate_scalar_types!(known_hash) if strict_types?
45
73
  # Give Shale the data it knows about, without extra attributes
46
74
  instance = super(known_hash, *args, **kwargs)
47
75
  instance.extra_attributes = extra_hash unless extra_hash.empty?
@@ -81,6 +109,30 @@ module Cline
81
109
 
82
110
  private
83
111
 
112
+ # Validate that scalar attributes do not receive a structured value.
113
+ #
114
+ # @param known_hash [Hash] The known attributes to validate
115
+ # @return [void]
116
+ def validate_scalar_types!(known_hash)
117
+ known_hash.each do |key, value|
118
+ attribute = attributes[hash_mapping.keys[key].attribute]
119
+ check_scalar_value!(attribute, value) if attribute
120
+ end
121
+ end
122
+
123
+ # Check that a value is structurally compatible with an attribute.
124
+ # A scalar attribute should not receive an Array or a Hash.
125
+ #
126
+ # @param attribute [Shale::Attribute] The attribute to check
127
+ # @param value [Object] The raw value to check
128
+ # @return [void]
129
+ def check_scalar_value!(attribute, value)
130
+ return if value.nil? || attribute.collection? || attribute.type < Shale::Type::Complex
131
+ return unless value.is_a?(Array) || value.is_a?(Hash)
132
+
133
+ raise TypeError, "Attribute #{attribute.name.inspect} of #{name} expects a scalar value but got a #{value.class}"
134
+ end
135
+
84
136
  # Complete the hash mapping to include camelCase to snake_case as the defaults.
85
137
  # Do it only once.
86
138
  def complete_hash_mapping
@@ -46,14 +46,65 @@ module Cline
46
46
  # @!group Public API
47
47
 
48
48
  # An input used for tool use content.
49
- class ToolUseInput < Schema
49
+ # Its values are filled by the LLM, whose types are not guaranteed by the Cline CLI:
50
+ # deserialization is therefore lenient (wrong values do not raise but are stored in {#wrong_attributes}).
51
+ class ToolUseInput < LenientSchema
50
52
  # @!group Public API
51
53
 
54
+ # A file to be read by the read_files tool
55
+ # The path is expected to be a String: a wrong type (e.g. an Array) is reported as a deserialization error.
56
+ class FileInput < Schema
57
+ # @!group Public API
58
+
59
+ strict_types!
60
+
61
+ # @return [String] File path
62
+ attribute :path, :string
63
+ end
64
+
52
65
  # @return [String] Question
53
66
  attribute :question, :string
54
67
 
55
68
  # @return [Array<String>] List of options for the given question
56
69
  attribute :options, Utils::Schema.collection(:string)
70
+
71
+ # @return [String, nil] File path (for editor / read_files tools)
72
+ attribute :path, :string
73
+
74
+ # @return [String, nil] New file content (for editor tool)
75
+ attribute :new_text, :string
76
+
77
+ # @return [String, nil] Old file content (for editor tool)
78
+ attribute :old_text, :string
79
+
80
+ # @return [String, nil] Shell commands to run (for run_commands tool)
81
+ attribute :commands, :string
82
+
83
+ # @return [Array<FileInput>, nil] Files to read (for read_files tool)
84
+ attribute :files, Utils::Schema.collection(FileInput)
85
+
86
+ cline_snake_attributes :new_text, :old_text
87
+ end
88
+
89
+ # A content entry of a tool result that is not a plain String.
90
+ class ToolResultContent < Schema
91
+ # @!group Public API
92
+
93
+ # @return [String, nil] Query or command executed by the tool
94
+ attribute :query, :string
95
+
96
+ # @return [String, nil] Result of the query or command
97
+ attribute :result, :string
98
+
99
+ # @return [Boolean, nil] Whether the query or command succeeded
100
+ attribute :success, :boolean
101
+
102
+ # Get a human-readable representation of this tool result.
103
+ #
104
+ # @return [String] The result of the tool, or its query if there is no result
105
+ def to_s
106
+ result || query || ''
107
+ end
57
108
  end
58
109
 
59
110
  # @return [String] Content block type (text, tool_use, tool_result)
@@ -62,6 +113,9 @@ module Cline
62
113
  # @return [String, nil] Text content (for type "text")
63
114
  attribute :text, :string
64
115
 
116
+ # @return [String, nil] Thinking content (for type "thinking")
117
+ attribute :thinking, :string
118
+
65
119
  # @return [String, nil] Tool use identifier (for type "tool_use")
66
120
  attribute :id, :string
67
121
 
@@ -74,8 +128,8 @@ module Cline
74
128
  # @return [String, nil] Tool use identifier this result corresponds to (for type "tool_result")
75
129
  attribute :tool_use_id, :string
76
130
 
77
- # @return [String, nil] Content of the tool result (for type "tool_result")
78
- attribute :content, :string
131
+ # @return [String, Array<ToolResultContent>, nil] Content of the tool result (for type "tool_result")
132
+ attribute :content, Utils::Schema.union(:string, Utils::Schema.collection(ToolResultContent))
79
133
 
80
134
  cline_snake_attributes :tool_use_id
81
135
  end
@@ -141,7 +195,7 @@ module Cline
141
195
  "Assistant: #{first_text}"
142
196
  end
143
197
  when 'tool'
144
- "Tool result: #{content.find { |c| c.type == 'tool_result' }&.content}"
198
+ "Tool result: #{human_tool_result}"
145
199
  else
146
200
  to_s
147
201
  end
@@ -174,5 +228,15 @@ module Cline
174
228
  def first_text
175
229
  content.find { |c| c.type == 'text' }&.text.to_s.gsub("\n", ' ')
176
230
  end
231
+
232
+ # Get a human representation of the tool result content of this message.
233
+ #
234
+ # @return [String] The tool result content, as a String
235
+ def human_tool_result
236
+ tool_result = content.find { |c| c.type == 'tool_result' }&.content
237
+ return tool_result.to_s if tool_result.nil? || tool_result.is_a?(String)
238
+
239
+ Array(tool_result).join(' ')
240
+ end
177
241
  end
178
242
  end
@@ -7,6 +7,23 @@ module Cline
7
7
 
8
8
  # @!group Public API
9
9
 
10
+ # Origin of the messages (from where they were generated)
11
+ class Origin < Schema
12
+ # @!group Public API
13
+
14
+ # @return [String] Source of the messages (cli or api)
15
+ attribute :source, :string
16
+
17
+ # @return [String] Interaction mode (user or auto)
18
+ attribute :mode, :string
19
+
20
+ # @return [String] Origin session ID
21
+ attribute :session_id, :string
22
+
23
+ # @return [String] Version of the origin source
24
+ attribute :version, :string
25
+ end
26
+
10
27
  Serializable::ClineData.include_for(self, proc { |base_dir| "#{File.basename(base_dir)}.messages.json" })
11
28
 
12
29
  # @return [Integer] Schema version
@@ -21,6 +38,15 @@ module Cline
21
38
  # @return [String] Session ID
22
39
  attribute :session_id, :string
23
40
 
41
+ # @return [String, nil] Source of the messages (cli or api)
42
+ attribute :source, :string
43
+
44
+ # @return [String, nil] Interaction mode (user or auto)
45
+ attribute :mode, :string
46
+
47
+ # @return [Origin, nil] Origin of the messages
48
+ attribute :origin, Origin
49
+
24
50
  # @return [Array<SessionMessage>] Messages of this session
25
51
  attribute :messages, Utils::Schema.collection(SessionMessage)
26
52
 
@@ -74,6 +74,14 @@ module Cline
74
74
  subclass.value_type = value_type
75
75
  end
76
76
 
77
+ # Check if a raw value can be deserialized by this map.
78
+ #
79
+ # @param raw_value [Object] The raw value to check
80
+ # @return [Boolean] true if this map can deserialize this value
81
+ def accepts_value?(raw_value)
82
+ raw_value.is_a?(self) || raw_value.is_a?(Hash)
83
+ end
84
+
77
85
  # Parse a Hash object and instantiate the proper instance from it.
78
86
  #
79
87
  # @param hash [Hash] Data
@@ -233,6 +241,14 @@ module Cline
233
241
  subclass.value_type = value_type
234
242
  end
235
243
 
244
+ # Check if a raw value can be deserialized by this collection.
245
+ #
246
+ # @param raw_value [Object] The raw value to check
247
+ # @return [Boolean] true if this collection can deserialize this value
248
+ def accepts_value?(raw_value)
249
+ raw_value.is_a?(self) || raw_value.is_a?(Array)
250
+ end
251
+
236
252
  # Parse a Ruby object and instantiate the proper instance from it.
237
253
  #
238
254
  # @param hash [Object] Data
@@ -285,6 +301,145 @@ module Cline
285
301
  end
286
302
  Schema.const_get(external_name)
287
303
  end
304
+
305
+ # Provide a class that can be used by Shale for attributes that can be of several types, like this:
306
+ # "content": "Some text" # A String
307
+ # "content": [{ "query": "ls" }, ...] # An Array of structures
308
+ # The first type accepting a value is used to deserialize / serialize it.
309
+ #
310
+ # @param shale_types [Array<Symbol, Class>] The Shale types this attribute can be (at least 2)
311
+ # @return [Class] Class that can be used in an attribute declaration
312
+ def self.union(*shale_types)
313
+ Union.of(*shale_types)
314
+ end
315
+
316
+ # Handle union types: attributes that can be of several types.
317
+ # Used by {Cline::Utils::Schema.union}.
318
+ class Union
319
+ # @!group Internal
320
+
321
+ # Ruby classes that can be deserialized by each scalar Shale type.
322
+ # Shale types not listed here are considered to accept any value.
323
+ SCALAR_VALUE_CLASSES = {
324
+ Shale::Type::String => String,
325
+ Shale::Type::Integer => Integer,
326
+ Shale::Type::Float => Float,
327
+ Shale::Type::Boolean => [TrueClass, FalseClass],
328
+ Shale::Type::Date => Date,
329
+ Shale::Type::Time => Time,
330
+ Shale::Type::Decimal => BigDecimal
331
+ }.freeze
332
+
333
+ class << self
334
+ # Build (or retrieve) the type class for a union of the given types.
335
+ #
336
+ # @param shale_types [Array<Symbol, Class>] The Shale types this union can be (at least 2)
337
+ # @return [Class] Class that can be used in an attribute declaration
338
+ def of(*shale_types)
339
+ raise ArgumentError, "A union type needs at least 2 types, got #{shale_types.size}" if shale_types.size < 2
340
+
341
+ shale_types = shale_types.map { |shale_type| shale_type.is_a?(Symbol) ? Shale::Type.lookup(shale_type) : shale_type }
342
+ # Give a name to this class because Shale needs it to reference it in its accessors.
343
+ # That also allows us to not redefine the same class several times.
344
+ external_name = :"UnionOf#{shale_types.map { |shale_type| shale_type.to_s.gsub(':', '') }.join('Or')}"
345
+ unless Cline::Utils::Schema.const_defined?(external_name)
346
+ union_class = new_union_class
347
+ union_class.union_types = shale_types
348
+ union_class.external_name = external_name
349
+ Cline::Utils::Schema.const_set(external_name, union_class)
350
+ end
351
+ Cline::Utils::Schema.const_get(external_name)
352
+ end
353
+
354
+ # Check if a raw value can be deserialized by a given Shale type.
355
+ # Shale types that don't handle accepted values are considered as scalar types.
356
+ #
357
+ # @param shale_type [Class] The Shale type to check
358
+ # @param raw_value [Object] The raw value to check
359
+ # @return [Boolean] true if the Shale type can deserialize this value
360
+ def accepts_value?(shale_type, raw_value)
361
+ if shale_type.respond_to?(:accepts_value?)
362
+ shale_type.accepts_value?(raw_value)
363
+ else
364
+ raw_value.is_a?(SCALAR_VALUE_CLASSES.fetch(shale_type, Object))
365
+ end
366
+ end
367
+
368
+ private
369
+
370
+ # Create the class implementing a union type.
371
+ #
372
+ # @return [Class] The new union type class
373
+ def new_union_class
374
+ Class.new(Cline::Schema) do
375
+ class << self
376
+ attr_accessor :union_types, :external_name
377
+
378
+ # Check if a raw value can be deserialized by one of this union's types.
379
+ #
380
+ # @param raw_value [Object] The raw value to check
381
+ # @return [Boolean] true if one of this union's types accepts this value
382
+ def accepts_value?(raw_value)
383
+ raw_value.is_a?(self) ||
384
+ union_types.any? { |union_type| Cline::Utils::Schema::Union.accepts_value?(union_type, raw_value) }
385
+ end
386
+
387
+ # Parse a value and instantiate the proper instance from it.
388
+ #
389
+ # @param value [Object] Data
390
+ # @param args [Array] Remaining arguments to be transferred to Shale
391
+ # @param kwargs [Hash] Remaining kwargs to be transferred to Shale
392
+ # @return [Object, nil] Corresponding instance, or nil if value is nil
393
+ def of_hash(value, *args, **kwargs)
394
+ return nil if value.nil?
395
+
396
+ union_type_for(value).of_hash(value, *args, **kwargs)
397
+ end
398
+
399
+ # Get a Ruby object from an instance.
400
+ #
401
+ # @param value [Object] Object to serialize
402
+ # @param args [Array] Remaining arguments to be transferred to Shale
403
+ # @param kwargs [Hash] Remaining kwargs to be transferred to Shale
404
+ # @return [Object, nil] Corresponding Ruby object, or nil if value is nil
405
+ def as_hash(value, *args, **kwargs)
406
+ return nil if value.nil?
407
+
408
+ union_type_for(value).as_hash(value, *args, **kwargs)
409
+ end
410
+
411
+ # Cast an input value to the type matching it.
412
+ #
413
+ # @param value [Object, nil] The value to cast
414
+ # @return [Object, nil] The corresponding instance, or nil if none
415
+ def cast(value)
416
+ return nil if value.nil?
417
+
418
+ union_type_for(value).cast(value)
419
+ end
420
+
421
+ # Return the class name
422
+ #
423
+ # @return [String] The class name
424
+ def to_s
425
+ "::Cline::Utils::Schema::#{@external_name}"
426
+ end
427
+
428
+ private
429
+
430
+ # Get the type that can deserialize / serialize a given value.
431
+ #
432
+ # @param value [Object] The value to find the type of
433
+ # @return [Class] The corresponding type
434
+ def union_type_for(value)
435
+ union_types.find { |union_type| Cline::Utils::Schema::Union.accepts_value?(union_type, value) } ||
436
+ raise("Unable to cast a #{value.class} into #{name}")
437
+ end
438
+ end
439
+ end
440
+ end
441
+ end
442
+ end
288
443
  end
289
444
  end
290
445
  end
data/lib/cline/version.rb CHANGED
@@ -2,5 +2,5 @@ module Cline
2
2
  # @!group Public API
3
3
 
4
4
  # Gem version
5
- VERSION = '2.1.1'
5
+ VERSION = '2.3.0'
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cline-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.1
4
+ version: 2.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Muriel Salvan
@@ -187,6 +187,7 @@ files:
187
187
  - lib/cline/global_state/models.rb
188
188
  - lib/cline/global_state/toggles.rb
189
189
  - lib/cline/global_state/workspace.rb
190
+ - lib/cline/lenient_schema.rb
190
191
  - lib/cline/log.rb
191
192
  - lib/cline/logs.rb
192
193
  - lib/cline/mcp_settings.rb