baml-cc 0.208.5

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.
@@ -0,0 +1,651 @@
1
+ use std::result::Result;
2
+
3
+ use baml_types::{BamlMap, BamlValue, BamlValueWithMeta, ResponseCheck};
4
+ use indexmap::IndexMap;
5
+ use jsonish::{ResponseBamlValue, ResponseValueMeta};
6
+ use magnus::{
7
+ prelude::*, typed_data::Obj, value::Value, Error, Float, Integer, IntoValue, RArray, RClass,
8
+ RHash, RModule, RString, Ruby, Symbol, TypedData,
9
+ };
10
+
11
+ use crate::types::{
12
+ self,
13
+ media::{Audio, Image},
14
+ };
15
+
16
+ struct SerializationError {
17
+ position: Vec<String>,
18
+ message: String,
19
+ }
20
+
21
+ pub struct RubyToJson<'rb> {
22
+ ruby: &'rb Ruby,
23
+ }
24
+
25
+ impl<'rb> RubyToJson<'rb> {
26
+ pub fn roundtrip(from: Value) -> crate::Result<Value> {
27
+ let json = RubyToJson::convert(from)?;
28
+ serde_magnus::serialize(&json)
29
+ }
30
+
31
+ /// Serialize a list of check results into some `Checked__*` instance.
32
+ pub fn serialize_response_checks(
33
+ ruby: &Ruby,
34
+ checks: &[ResponseCheck],
35
+ ) -> crate::Result<RHash> {
36
+ // Create a `Check` for each check in the `Checked__*`.
37
+ let hash = ruby.hash_new();
38
+ checks.iter().try_for_each(
39
+ |ResponseCheck {
40
+ name,
41
+ expression,
42
+ status,
43
+ }| {
44
+ let check_class = ruby.eval::<RClass>("Baml::Checks::Check")?;
45
+ let check_hash = ruby.hash_new();
46
+ check_hash.aset(ruby.sym_new("name"), name.as_str())?;
47
+ check_hash.aset(ruby.sym_new("expr"), expression.as_str())?;
48
+ check_hash.aset(ruby.sym_new("status"), status.as_str())?;
49
+
50
+ let check: Value = check_class.funcall("new", (check_hash,))?;
51
+ hash.aset(ruby.sym_new(name.as_str()), check)?;
52
+ crate::Result::Ok(())
53
+ },
54
+ )?;
55
+
56
+ Ok(hash)
57
+ }
58
+
59
+ pub fn serialize_baml(
60
+ ruby: &Ruby,
61
+ types: RModule,
62
+ partial_types: RModule,
63
+ allow_partials: bool,
64
+ mut from: ResponseBamlValue,
65
+ ) -> crate::Result<Value> {
66
+ let allow_partials = allow_partials && !from.0.meta().2.required_done;
67
+ // If we encounter a BamlValue node with check results, serialize it as
68
+ // { value: T, checks: K }. To compute `value`, we strip the metadata
69
+ // off the node and pass it back to `serialize_baml`.
70
+ let ResponseValueMeta(_flags, checks, completion, ..) = from.0.meta_mut();
71
+
72
+ if completion.display && allow_partials {
73
+ let hash = ruby.hash_new();
74
+ let stream_state_class = ruby.eval::<RClass>("Baml::StreamState")?;
75
+ hash.aset(
76
+ ruby.sym_new("state"),
77
+ ruby.sym_new(
78
+ serde_json::to_string(&completion.state)
79
+ .expect("Serializing CompletionState is safe."),
80
+ ),
81
+ )?;
82
+ completion.display = false;
83
+ let serialized_subvalue =
84
+ RubyToJson::serialize_baml(ruby, types, partial_types, allow_partials, from)?;
85
+ hash.aset(ruby.sym_new("value"), serialized_subvalue)?;
86
+ let res = stream_state_class.funcall("new", (hash,));
87
+ Ok(res?)
88
+ }
89
+ // Otherwise encode it directly.
90
+ else if !checks.is_empty() {
91
+ let serialized_checks = Self::serialize_response_checks(ruby, checks)?;
92
+
93
+ checks.clear();
94
+
95
+ let serialized_subvalue =
96
+ Self::serialize_baml(ruby, types, partial_types, allow_partials, from)?;
97
+
98
+ let checked_class = ruby.eval::<RClass>("Baml::Checked")?;
99
+ let hash = ruby.hash_new();
100
+ hash.aset(ruby.sym_new("value"), serialized_subvalue)?;
101
+ if !serialized_checks.is_empty() {
102
+ hash.aset(ruby.sym_new("checks"), serialized_checks)?;
103
+ }
104
+ let res = checked_class.funcall("new", (hash,));
105
+ Ok(res?)
106
+ }
107
+ // Otherwise encode it directly.
108
+ else {
109
+ let res = match from.0 {
110
+ BamlValueWithMeta::Class(class_name, class_fields, _) => {
111
+ let hash = ruby.hash_new();
112
+ for (k, v) in class_fields.into_iter() {
113
+ let subvalue_allow_partials = allow_partials && !v.meta().2.required_done;
114
+ let k = ruby.sym_new(k.as_str());
115
+ let v = RubyToJson::serialize_baml(
116
+ ruby,
117
+ types,
118
+ partial_types,
119
+ subvalue_allow_partials,
120
+ ResponseBamlValue(v),
121
+ )?;
122
+ hash.aset(k, v)?;
123
+ }
124
+
125
+ let (preferred_module, backup_module) = if allow_partials {
126
+ (partial_types, types)
127
+ } else {
128
+ (types, partial_types)
129
+ };
130
+ let preferred_class =
131
+ match preferred_module.const_get::<_, RClass>(class_name.as_str()) {
132
+ Ok(class_type) => class_type,
133
+ Err(_) => ruby.eval::<RClass>("Baml::DynamicStruct")?,
134
+ };
135
+ let backup_class =
136
+ match backup_module.const_get::<_, RClass>(class_name.as_str()) {
137
+ Ok(class_type) => class_type,
138
+ Err(_) => ruby.eval::<RClass>("Baml::DynamicStruct")?,
139
+ };
140
+ match preferred_class.funcall("new", (hash,)) {
141
+ Ok(res) => Ok(res),
142
+ Err(original_error) => match backup_class.funcall("new", (hash,)) {
143
+ Ok(res) => Ok(res),
144
+ Err(_) => Err(original_error),
145
+ },
146
+ }
147
+ }
148
+ BamlValueWithMeta::Enum(enum_name, enum_value, _) => {
149
+ if let Ok(enum_type) = types.const_get::<_, RClass>(enum_name.as_str()) {
150
+ let enum_value = ruby.str_new(&enum_value);
151
+ if let Ok(enum_instance) = enum_type.funcall("deserialize", (enum_value,)) {
152
+ return Ok(enum_instance);
153
+ }
154
+ }
155
+
156
+ Ok(ruby.str_new(&enum_value).into_value_with(ruby))
157
+ }
158
+ BamlValueWithMeta::Map(m, _) => {
159
+ let hash = ruby.hash_new();
160
+ for (k, v) in m.into_iter() {
161
+ let k = ruby.str_new(&k);
162
+ let v = RubyToJson::serialize_baml(
163
+ ruby,
164
+ types,
165
+ partial_types,
166
+ allow_partials,
167
+ ResponseBamlValue(v),
168
+ )?;
169
+ hash.aset(k, v)?;
170
+ }
171
+ Ok(hash.into_value_with(ruby))
172
+ }
173
+ BamlValueWithMeta::List(l, _) => {
174
+ let arr = ruby.ary_new();
175
+ for v in l.into_iter() {
176
+ let v = RubyToJson::serialize_baml(
177
+ ruby,
178
+ types,
179
+ partial_types,
180
+ allow_partials,
181
+ ResponseBamlValue(v),
182
+ )?;
183
+ arr.push(v)?;
184
+ }
185
+ Ok(arr.into_value_with(ruby))
186
+ }
187
+ _ => serde_magnus::serialize(&from.0.value()),
188
+ };
189
+ res
190
+ }
191
+ }
192
+
193
+ pub fn serialize(
194
+ ruby: &Ruby,
195
+ types: RModule,
196
+ partial_types: RModule,
197
+ allow_partials: bool,
198
+ from: Value,
199
+ ) -> crate::Result<Value> {
200
+ let json = RubyToJson::convert(from)?;
201
+ RubyToJson::serialize_baml(
202
+ ruby,
203
+ types,
204
+ partial_types,
205
+ allow_partials,
206
+ ResponseBamlValue(BamlValueWithMeta::with_default_meta(&json)),
207
+ )
208
+ }
209
+
210
+ /// Convert a Ruby object to a JSON object.
211
+ ///
212
+ /// We have to implement this ourselves instead of relying on Serde, because in the codegen,
213
+ /// we can't convert a BAML-generated type to a hash trivially (specifically union-typed
214
+ /// fields do not serialize correctly, see https://sorbet.org/docs/tstruct#serialize-gotchas)
215
+ ///
216
+ /// We do still rely on :serialize for enums though.
217
+ pub fn convert(from: Value) -> crate::Result<BamlValue> {
218
+ let ruby = Ruby::get_with(from);
219
+ let result = RubyToJson { ruby: &ruby }.to_json(from, vec![]);
220
+
221
+ match result {
222
+ Ok(value) => Ok(value),
223
+ Err(e) => {
224
+ let mut errors = vec![];
225
+ for error in e {
226
+ errors.push(format!(" {}: {}", error.position.join("."), error.message));
227
+ }
228
+ Err(Error::new(
229
+ ruby.exception_type_error(),
230
+ format!(
231
+ "failed to convert Ruby object to JSON, errors were:\n{}\nRuby object:\n{}",
232
+ errors.join("\n"),
233
+ from.inspect()
234
+ ),
235
+ ))
236
+ }
237
+ }
238
+ }
239
+
240
+ pub fn convert_hash_to_json(from: RHash) -> crate::Result<IndexMap<String, BamlValue>> {
241
+ let ruby = Ruby::get_with(from);
242
+ let result = RubyToJson { ruby: &ruby }.hash_to_map(from, vec![]);
243
+
244
+ match result {
245
+ Ok(value) => Ok(value),
246
+ Err(e) => {
247
+ let mut errors = vec![];
248
+ for error in e {
249
+ errors.push(format!(" {}: {}", error.position.join("."), error.message));
250
+ }
251
+ Err(Error::new(
252
+ ruby.exception_type_error(),
253
+ format!(
254
+ "failed to convert Ruby object to JSON, errors were:\n{}\nRuby object:\n{}",
255
+ errors.join("\n"),
256
+ from.inspect()
257
+ ),
258
+ ))
259
+ }
260
+ }
261
+ }
262
+
263
+ fn to_json(
264
+ &self,
265
+ any: Value,
266
+ field_pos: Vec<String>,
267
+ ) -> Result<BamlValue, Vec<SerializationError>> {
268
+ if any.is_nil() {
269
+ return Ok(BamlValue::Null);
270
+ }
271
+
272
+ if any.is_kind_of(self.ruby.class_true_class()) {
273
+ return Ok(BamlValue::Bool(true));
274
+ }
275
+
276
+ if any.is_kind_of(self.ruby.class_false_class()) {
277
+ return Ok(BamlValue::Bool(false));
278
+ }
279
+
280
+ if let Some(any) = magnus::Integer::from_value(any) {
281
+ return self.to_int(any, field_pos);
282
+ }
283
+
284
+ if let Some(any) = magnus::Float::from_value(any) {
285
+ return self.to_float(any, field_pos);
286
+ }
287
+
288
+ if let Some(any) = RString::from_value(any) {
289
+ return self.to_string(any, field_pos).map(BamlValue::String);
290
+ }
291
+
292
+ if let Some(any) = RArray::from_value(any) {
293
+ return self.to_array(any, field_pos);
294
+ }
295
+
296
+ if let Some(any) = RHash::from_value(any) {
297
+ return self.hash_to_map(any, field_pos).map(BamlValue::Map);
298
+ }
299
+
300
+ if let Ok(superclass) = any.class().superclass() {
301
+ let superclass = unsafe { superclass.name() };
302
+
303
+ if superclass == "T::Struct" {
304
+ return self.struct_to_map(any, field_pos);
305
+ }
306
+
307
+ if superclass == "T::Enum" {
308
+ return self.sorbet_to_json(any, field_pos);
309
+ }
310
+ }
311
+
312
+ if self.is_type::<Audio>(any) {
313
+ return self.to_type::<Audio>(any, field_pos);
314
+ }
315
+
316
+ if self.is_type::<Image>(any) {
317
+ return self.to_type::<Image>(any, field_pos);
318
+ }
319
+
320
+ Err(vec![SerializationError {
321
+ position: field_pos,
322
+ message: format!(
323
+ "JSON conversion not supported for value of type {:?}",
324
+ any.class()
325
+ ),
326
+ }])
327
+ }
328
+
329
+ fn to_int(
330
+ &self,
331
+ any: Integer,
332
+ field_pos: Vec<String>,
333
+ ) -> Result<BamlValue, Vec<SerializationError>> {
334
+ if let Ok(any) = any.to_i64() {
335
+ return Ok(BamlValue::Int(any));
336
+ }
337
+
338
+ Err(vec![SerializationError {
339
+ position: field_pos,
340
+ message: format!("failed to convert {any:?} to i64"),
341
+ }])
342
+ }
343
+
344
+ fn to_float(&self, any: Float, _: Vec<String>) -> Result<BamlValue, Vec<SerializationError>> {
345
+ Ok(BamlValue::Float(any.to_f64()))
346
+ }
347
+
348
+ fn to_string(
349
+ &self,
350
+ any: RString,
351
+ field_pos: Vec<String>,
352
+ ) -> Result<String, Vec<SerializationError>> {
353
+ let Ok(any) = any.to_string() else {
354
+ return Err(vec![SerializationError {
355
+ position: field_pos,
356
+ message: format!("cannot convert {any:#?} to utf-8 string"),
357
+ }]);
358
+ };
359
+ Ok(any)
360
+ }
361
+
362
+ fn to_array(
363
+ &self,
364
+ any: RArray,
365
+ field_pos: Vec<String>,
366
+ ) -> Result<BamlValue, Vec<SerializationError>> {
367
+ let mut errs = vec![];
368
+ let mut arr = vec![];
369
+
370
+ for (i, value) in any.enumeratorize("each", ()).enumerate() {
371
+ let mut field_pos = field_pos.clone();
372
+ field_pos.push(i.to_string());
373
+
374
+ let Ok(value) = value else {
375
+ errs.push(SerializationError {
376
+ position: field_pos.clone(),
377
+ message: format!("failed to enumerate array element at index {i}"),
378
+ });
379
+ continue;
380
+ };
381
+ match self.to_json(value, field_pos) {
382
+ Ok(json_value) => {
383
+ arr.push(json_value);
384
+ }
385
+ Err(e) => errs.extend(e),
386
+ }
387
+ }
388
+
389
+ if !errs.is_empty() {
390
+ return Err(errs);
391
+ }
392
+
393
+ Ok(BamlValue::List(arr))
394
+ }
395
+
396
+ fn hash_key_to_string(
397
+ &self,
398
+ k: Value,
399
+ field_pos: Vec<String>,
400
+ ) -> Result<String, Vec<SerializationError>> {
401
+ if let Some(k) = Symbol::from_value(k) {
402
+ return match k.name() {
403
+ Ok(k) => Ok(k.to_string()),
404
+ Err(_) => Err(vec![SerializationError {
405
+ position: field_pos.clone(),
406
+ message: format!("failed to convert symbol key in hash to string: {k:#?}"),
407
+ }]),
408
+ };
409
+ }
410
+ if let Some(k) = RString::from_value(k) {
411
+ let mut field_pos = field_pos.clone();
412
+ field_pos.push(format!("{k:#?}"));
413
+ return match self.to_string(k, field_pos) {
414
+ Ok(k) => Ok(k),
415
+ Err(errs) => Err(errs
416
+ .into_iter()
417
+ .map(|mut e| {
418
+ e.message =
419
+ format!("failed to convert string key in hash to string: {k:#?}");
420
+ e
421
+ })
422
+ .collect()),
423
+ };
424
+ }
425
+ Err(vec![SerializationError {
426
+ position: field_pos.clone(),
427
+ message: format!(
428
+ "expected every key in this hash to be symbol or string, but found key {k:#?}"
429
+ ),
430
+ }])
431
+ }
432
+
433
+ fn hash_to_map(
434
+ &self,
435
+ any: RHash,
436
+ field_pos: Vec<String>,
437
+ ) -> Result<BamlMap<String, BamlValue>, Vec<SerializationError>> {
438
+ use magnus::r_hash::ForEach;
439
+
440
+ let mut errs = vec![];
441
+ let mut map = BamlMap::new();
442
+ if any
443
+ .foreach(|k: Value, v: Value| {
444
+ let k = match self.hash_key_to_string(k, field_pos.clone()) {
445
+ Ok(k) => k,
446
+ Err(e) => {
447
+ errs.extend(e);
448
+ return Ok(ForEach::Continue);
449
+ }
450
+ };
451
+
452
+ let mut field_pos = field_pos.clone();
453
+ field_pos.push(k.clone());
454
+
455
+ match self.to_json(v, field_pos.clone()) {
456
+ Ok(json_value) => {
457
+ map.insert(k.to_string(), json_value);
458
+ }
459
+ Err(e) => errs.extend(e),
460
+ }
461
+ Ok(ForEach::Continue)
462
+ })
463
+ .is_err()
464
+ {
465
+ errs.push(SerializationError {
466
+ position: field_pos.clone(),
467
+ message: "failed to iterate over hash".to_string(),
468
+ });
469
+ };
470
+
471
+ if !errs.is_empty() {
472
+ return Err(errs);
473
+ }
474
+
475
+ Ok(map)
476
+ }
477
+
478
+ fn struct_to_map(
479
+ &self,
480
+ any: Value,
481
+ field_pos: Vec<String>,
482
+ ) -> Result<BamlValue, Vec<SerializationError>> {
483
+ // https://ruby-doc.org/3.0.4/Module.html#method-i-instance_methods
484
+ let fields = match any
485
+ .class()
486
+ .check_funcall::<_, _, Value>("instance_methods", (self.ruby.qfalse(),))
487
+ {
488
+ None => {
489
+ return Err(vec![SerializationError {
490
+ position: field_pos,
491
+ message: "class does not respond to :instance_methods".to_string(),
492
+ }]);
493
+ }
494
+ Some(Err(e)) => {
495
+ return Err(vec![SerializationError {
496
+ position: field_pos,
497
+ message: format!(".class.instance_methods returned error: {e}"),
498
+ }]);
499
+ }
500
+ Some(Ok(fields)) => match RArray::from_value(fields) {
501
+ None => {
502
+ return Err(vec![SerializationError {
503
+ position: field_pos,
504
+ message: format!(".class.instance_methods was non-array: {fields:?}"),
505
+ }]);
506
+ }
507
+ Some(fields) => {
508
+ let fields = fields
509
+ .enumeratorize("each", ())
510
+ .collect::<crate::Result<Vec<_>>>();
511
+ let fields = match fields {
512
+ Err(e) => {
513
+ return Err(vec![SerializationError {
514
+ position: field_pos,
515
+ message: format!(".class.instance_methods.each failed: {e:?}"),
516
+ }]);
517
+ }
518
+ Ok(fields) => fields,
519
+ };
520
+ let fields = fields
521
+ .into_iter()
522
+ .map(|v| {
523
+ Symbol::from_value(v)
524
+ .ok_or(format!("failed to convert {v:#?} to symbol"))
525
+ })
526
+ .collect::<Result<Vec<_>, std::string::String>>();
527
+ match fields {
528
+ Err(e) => {
529
+ return Err(vec![SerializationError {
530
+ position: field_pos,
531
+ message: format!(
532
+ "failed to convert .class.instance_methods to array of symbols: {e}"
533
+ ),
534
+ }]);
535
+ }
536
+ Ok(fields) => fields
537
+ .into_iter()
538
+ .map(|s| s.to_string())
539
+ .collect::<Vec<String>>(),
540
+ }
541
+ }
542
+ },
543
+ };
544
+
545
+ let mut errs = vec![];
546
+ let mut map = BamlMap::new();
547
+ for field in fields.as_slice() {
548
+ let mut field_pos = field_pos.clone();
549
+ field_pos.push(field.clone());
550
+
551
+ let value = match any.funcall(field.clone(), ()) {
552
+ Ok(value) => value,
553
+ Err(e) => {
554
+ return Err(vec![SerializationError {
555
+ position: field_pos,
556
+ message: format!("object responded to :{field} with error: {e}"),
557
+ }]);
558
+ }
559
+ };
560
+
561
+ match self.to_json(value, field_pos) {
562
+ Ok(json_value) => {
563
+ map.insert(field.clone(), json_value);
564
+ }
565
+ Err(e) => {
566
+ errs.extend(e);
567
+ }
568
+ };
569
+ }
570
+
571
+ if !errs.is_empty() {
572
+ return Err(errs);
573
+ }
574
+
575
+ let fully_qualified_class_name = unsafe { any.class().name() }.into_owned();
576
+ let class_name = match fully_qualified_class_name.rsplit_once("::") {
577
+ Some((_, class_name)) => class_name.to_string(),
578
+ None => fully_qualified_class_name,
579
+ };
580
+ Ok(BamlValue::Class(class_name, map))
581
+
582
+ //Ok(BamlValue::Map(map))
583
+ }
584
+
585
+ // This codepath is not used right now - it was implemented as a proof-of-concept
586
+ // for serializing structs to JSON, by combining :to_hash with this method. If the
587
+ // implementation of struct_to_map proves to be stable, we can get rid of this.
588
+ #[allow(dead_code)]
589
+ fn struct_to_map_via_to_hash(
590
+ &self,
591
+ any: Result<Value, Error>,
592
+ field_pos: Vec<String>,
593
+ ) -> Result<BamlValue, Vec<SerializationError>> {
594
+ let any = match any {
595
+ Ok(any) => any,
596
+ Err(e) => {
597
+ return Err(vec![SerializationError {
598
+ position: field_pos,
599
+ message: format!("struct responded to :to_hash with error: {e}"),
600
+ }])
601
+ }
602
+ };
603
+
604
+ if let Some(any) = RHash::from_value(any) {
605
+ return self.hash_to_map(any, field_pos).map(BamlValue::Map);
606
+ }
607
+
608
+ Err(vec![SerializationError {
609
+ position: field_pos,
610
+ message: format!("struct did not respond to :to_hash with a hash, was: {any:#?}"),
611
+ }])
612
+ }
613
+
614
+ fn is_type<T: TypedData>(&self, any: Value) -> bool {
615
+ any.class()
616
+ .eql(T::class(&Ruby::get_with(any)))
617
+ .is_ok_and(|is_eql| is_eql)
618
+ }
619
+
620
+ fn to_type<T: TypedData + types::media::CloneAsBamlValue>(
621
+ &self,
622
+ any: Value,
623
+ field_pos: Vec<String>,
624
+ ) -> Result<BamlValue, Vec<SerializationError>> {
625
+ match Obj::<T>::try_convert(any) {
626
+ Ok(o) => Ok(o.clone_as_baml_value()),
627
+ Err(e) => Err(vec![SerializationError {
628
+ position: field_pos,
629
+ message: format!("failed to convert {}: {:#?}", any.class(), e),
630
+ }]),
631
+ }
632
+ }
633
+
634
+ fn sorbet_to_json(
635
+ &self,
636
+ any: Value,
637
+ field_pos: Vec<String>,
638
+ ) -> Result<BamlValue, Vec<SerializationError>> {
639
+ match any.check_funcall("serialize", ()) {
640
+ None => Err(vec![SerializationError {
641
+ position: field_pos,
642
+ message: "object does not respond to :serialize".to_string(),
643
+ }]),
644
+ Some(Err(e)) => Err(vec![SerializationError {
645
+ position: field_pos,
646
+ message: format!("object responded to :serialize with error: {e}"),
647
+ }]),
648
+ Some(Ok(any)) => self.to_json(any, field_pos),
649
+ }
650
+ }
651
+ }