1extern crate self as gemstone_rs;
228
229pub mod bridge;
230pub mod browser;
231pub mod codegen;
232
233pub use bridge::{
234 BridgeDictionary, BridgeFieldRead, BridgeFieldWrite, BridgeKey, BridgeKeyType, BridgeMapped,
235 BridgeRoot, BridgeValue, DEFAULT_BRIDGE_ROOT,
236};
237pub use gemstone_gci::Oop;
238use gemstone_gci::{
239 char_from_oop, is_char, is_smallint, GciErrSType, GciLibrary, RawOop, GCI_ENCRYPT_BUF_SIZE,
240 GCI_INVALID_SESSION,
241};
242pub use gemstone_rs_macros::BridgeMapped;
243use std::cell::{Cell, RefCell};
244use std::collections::BTreeMap;
245use std::env;
246use std::error::Error as StdError;
247use std::ffi::{c_char, c_double, c_uint, CString, NulError};
248use std::fmt;
249use std::marker::PhantomData;
250use std::path::PathBuf;
251use std::rc::Rc;
252
253pub type Result<T> = std::result::Result<T, Error>;
254
255#[derive(Debug)]
256pub enum Error {
257 Gci(gemstone_gci::GciError),
258 MissingEnvironment(&'static str),
259 MissingConfig(&'static str),
260 Nul(NulError),
261 NotLoggedIn,
262 GemStone {
263 number: i32,
264 fatal: bool,
265 message: String,
266 },
267 IllegalOop {
268 operation: &'static str,
269 },
270 UnexpectedType {
271 expected: &'static str,
272 actual: String,
273 },
274 Mapping {
275 field: String,
276 expected: &'static str,
277 actual: String,
278 },
279 NegativeSize(i64),
280 ArgumentCountTooLarge(usize),
281}
282
283impl fmt::Display for Error {
284 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285 match self {
286 Self::Gci(err) => write!(f, "{err}"),
287 Self::MissingEnvironment(name) => {
288 write!(f, "missing required GemStone environment variable {name}")
289 }
290 Self::MissingConfig(name) => write!(f, "missing required GemStone config field {name}"),
291 Self::Nul(err) => write!(f, "string contains NUL byte: {err}"),
292 Self::NotLoggedIn => write!(f, "GemStone session is not logged in"),
293 Self::GemStone {
294 number,
295 fatal,
296 message,
297 } => write!(f, "GemStone error #{number} fatal={fatal}: {message}"),
298 Self::IllegalOop { operation } => {
299 write!(f, "GemStone operation {operation} returned OOP_ILLEGAL")
300 }
301 Self::UnexpectedType { expected, actual } => {
302 write!(f, "expected GemStone value type {expected}, got {actual}")
303 }
304 Self::Mapping {
305 field,
306 expected,
307 actual,
308 } => write!(
309 f,
310 "field {field} expected GemStone value type {expected}, got {actual}"
311 ),
312 Self::NegativeSize(size) => write!(f, "GemStone returned a negative size: {size}"),
313 Self::ArgumentCountTooLarge(count) => {
314 write!(f, "too many GemStone selector arguments: {count}")
315 }
316 }
317 }
318}
319
320impl StdError for Error {
321 fn source(&self) -> Option<&(dyn StdError + 'static)> {
322 match self {
323 Self::Gci(err) => Some(err),
324 Self::Nul(err) => Some(err),
325 Self::MissingEnvironment(_)
326 | Self::MissingConfig(_)
327 | Self::NotLoggedIn
328 | Self::GemStone { .. }
329 | Self::IllegalOop { .. }
330 | Self::UnexpectedType { .. }
331 | Self::Mapping { .. }
332 | Self::NegativeSize(_)
333 | Self::ArgumentCountTooLarge(_) => None,
334 }
335 }
336}
337
338impl From<gemstone_gci::GciError> for Error {
339 fn from(value: gemstone_gci::GciError) -> Self {
340 Self::Gci(value)
341 }
342}
343
344impl From<NulError> for Error {
345 fn from(value: NulError) -> Self {
346 Self::Nul(value)
347 }
348}
349
350#[derive(Clone, Debug, Eq, PartialEq)]
351pub struct Config {
352 pub stone: String,
353 pub netldi: String,
354 pub host: String,
355 pub username: String,
356 pub password: String,
357 pub host_username: String,
358 pub host_password: String,
359 pub gem_service: String,
360 pub lib_path: Option<PathBuf>,
361}
362
363impl Config {
364 pub fn builder() -> ConfigBuilder {
365 ConfigBuilder::default()
366 }
367
368 pub fn from_env() -> Result<Self> {
369 Self::builder()
370 .stone(
371 env::var("GS_STONE")
372 .or_else(|_| env::var("GS_STONE_NAME"))
373 .unwrap_or_else(|_| "gs64stone".to_string()),
374 )
375 .netldi(env::var("GS_NETLDI").unwrap_or_else(|_| "netldi".to_string()))
376 .host(env::var("GS_HOST").unwrap_or_else(|_| "localhost".to_string()))
377 .username(required_env("GS_USERNAME")?)
378 .password(required_env("GS_PASSWORD")?)
379 .host_username(env::var("GS_HOST_USERNAME").unwrap_or_default())
380 .host_password(env::var("GS_HOST_PASSWORD").unwrap_or_default())
381 .gem_service(env::var("GS_GEM_SERVICE").unwrap_or_else(|_| "gemnetobject".to_string()))
382 .maybe_lib_path(
383 env::var("GS_LIB_PATH")
384 .ok()
385 .filter(|value| !value.is_empty()),
386 )
387 .build()
388 }
389
390 pub fn stone_nrs(&self) -> String {
391 if !self.host.is_empty() && self.host != "localhost" && self.host != "127.0.0.1" {
392 format!("!@{}!{}!{}", self.host, self.netldi, self.stone)
393 } else {
394 self.stone.clone()
395 }
396 }
397}
398
399#[derive(Clone, Debug, Default, Eq, PartialEq)]
400pub struct ConfigBuilder {
401 stone: Option<String>,
402 netldi: Option<String>,
403 host: Option<String>,
404 username: Option<String>,
405 password: Option<String>,
406 host_username: Option<String>,
407 host_password: Option<String>,
408 gem_service: Option<String>,
409 lib_path: Option<PathBuf>,
410}
411
412impl ConfigBuilder {
413 pub fn stone(mut self, value: impl Into<String>) -> Self {
414 self.stone = Some(value.into());
415 self
416 }
417
418 pub fn netldi(mut self, value: impl Into<String>) -> Self {
419 self.netldi = Some(value.into());
420 self
421 }
422
423 pub fn host(mut self, value: impl Into<String>) -> Self {
424 self.host = Some(value.into());
425 self
426 }
427
428 pub fn username(mut self, value: impl Into<String>) -> Self {
429 self.username = Some(value.into());
430 self
431 }
432
433 pub fn password(mut self, value: impl Into<String>) -> Self {
434 self.password = Some(value.into());
435 self
436 }
437
438 pub fn host_username(mut self, value: impl Into<String>) -> Self {
439 self.host_username = Some(value.into());
440 self
441 }
442
443 pub fn host_password(mut self, value: impl Into<String>) -> Self {
444 self.host_password = Some(value.into());
445 self
446 }
447
448 pub fn gem_service(mut self, value: impl Into<String>) -> Self {
449 self.gem_service = Some(value.into());
450 self
451 }
452
453 pub fn lib_path(mut self, value: impl Into<PathBuf>) -> Self {
454 self.lib_path = Some(value.into());
455 self
456 }
457
458 pub fn maybe_lib_path(mut self, value: Option<impl Into<PathBuf>>) -> Self {
459 self.lib_path = value.map(Into::into);
460 self
461 }
462
463 pub fn build(self) -> Result<Config> {
464 let username = required_config("username", self.username)?;
465 let password = required_config("password", self.password)?;
466
467 Ok(Config {
468 stone: self.stone.unwrap_or_else(|| "gs64stone".to_string()),
469 netldi: self.netldi.unwrap_or_else(|| "netldi".to_string()),
470 host: self.host.unwrap_or_else(|| "localhost".to_string()),
471 username,
472 password,
473 host_username: self.host_username.unwrap_or_default(),
474 host_password: self.host_password.unwrap_or_default(),
475 gem_service: self
476 .gem_service
477 .unwrap_or_else(|| "gemnetobject".to_string()),
478 lib_path: self.lib_path,
479 })
480 }
481}
482
483impl Default for Config {
484 fn default() -> Self {
485 Self {
486 stone: "gs64stone".to_string(),
487 netldi: "netldi".to_string(),
488 host: "localhost".to_string(),
489 username: String::new(),
490 password: String::new(),
491 host_username: String::new(),
492 host_password: String::new(),
493 gem_service: "gemnetobject".to_string(),
494 lib_path: None,
495 }
496 }
497}
498
499#[derive(Clone, Copy, Debug, Eq, PartialEq)]
500pub enum TransactionPolicy {
501 Manual,
502 CommitOnSuccess,
503 AbortOnExit,
504}
505
506#[derive(Clone, Debug, PartialEq)]
507pub enum Value {
508 Nil,
509 Bool(bool),
510 SmallInt(i64),
511 Char(char),
512 String(String),
513 Oop(Oop),
514}
515
516impl Value {
517 pub fn as_oop(&self) -> Option<Oop> {
518 match self {
519 Self::Oop(oop) => Some(*oop),
520 _ => None,
521 }
522 }
523}
524
525pub struct Session {
526 lib: GciLibrary,
527 session_id: i32,
528 logged_in: Cell<bool>,
529 identity_map: RefCell<BTreeMap<RawOop, usize>>,
530 next_identity: Cell<usize>,
531 _not_send_sync: PhantomData<Rc<()>>,
532}
533
534impl Session {
535 pub fn login(config: Config) -> Result<Self> {
536 if config.username.is_empty() {
537 return Err(Error::MissingEnvironment("GS_USERNAME"));
538 }
539 if config.password.is_empty() {
540 return Err(Error::MissingEnvironment("GS_PASSWORD"));
541 }
542
543 let lib = GciLibrary::load(config.lib_path.clone())?;
544 let mut session = Self {
545 lib,
546 session_id: GCI_INVALID_SESSION as i32,
547 logged_in: Cell::new(false),
548 identity_map: RefCell::new(BTreeMap::new()),
549 next_identity: Cell::new(1),
550 _not_send_sync: PhantomData,
551 };
552 session.open(config)?;
553 Ok(session)
554 }
555
556 pub fn session_id(&self) -> i32 {
557 self.session_id
558 }
559
560 pub fn is_logged_in(&self) -> bool {
561 self.logged_in.get()
562 }
563
564 pub fn eval(&mut self, source: &str) -> Result<Value> {
565 let oop = self.eval_oop(source)?;
566 self.marshal(oop)
567 }
568
569 pub fn execute(&mut self, source: &str) -> Result<Oop> {
570 self.eval_oop(source)
571 }
572
573 pub fn eval_oop(&mut self, source: &str) -> Result<Oop> {
574 self.activate()?;
575 let source = CString::new(source)?;
576 let oop = unsafe { self.lib.gci_execute_str(&source, OOP_NIL)? };
577 self.check_oop("eval", oop)?;
578 Ok(Oop(oop))
579 }
580
581 pub fn perform(&mut self, receiver: Oop, selector: &str, args: &[Oop]) -> Result<Value> {
582 let oop = self.perform_oop(receiver, selector, args)?;
583 self.marshal(oop)
584 }
585
586 pub fn perform_oop(&mut self, receiver: Oop, selector: &str, args: &[Oop]) -> Result<Oop> {
587 self.activate()?;
588 let selector = CString::new(selector)?;
589 if args.len() > i32::MAX as usize {
590 return Err(Error::ArgumentCountTooLarge(args.len()));
591 }
592 let raw_args: Vec<RawOop> = args.iter().map(|oop| oop.raw()).collect();
593 let oop = unsafe {
594 self.lib.gci_perform(
595 receiver.raw(),
596 &selector,
597 raw_args.as_ptr(),
598 raw_args.len() as i32,
599 )?
600 };
601 self.check_oop("perform", oop)?;
602 Ok(Oop(oop))
603 }
604
605 pub fn resolve(&mut self, name: &str) -> Result<Oop> {
606 self.activate()?;
607 let name = CString::new(name)?;
608 let oop = unsafe { self.lib.gci_resolve_symbol(&name, OOP_NIL)? };
609 self.check_oop("resolve", oop)?;
610 Ok(Oop(oop))
611 }
612
613 pub fn new_string(&mut self, value: &str) -> Result<Oop> {
614 self.activate()?;
615 let value = CString::new(value)?;
616 let oop = unsafe { self.lib.gci_new_string(&value)? };
617 self.check_oop("new_string", oop)?;
618 Ok(Oop(oop))
619 }
620
621 pub fn new_symbol(&mut self, value: &str) -> Result<Oop> {
622 self.activate()?;
623 let value = CString::new(value)?;
624 let oop = unsafe { self.lib.gci_new_symbol(&value)? };
625 self.check_oop("new_symbol", oop)?;
626 Ok(Oop(oop))
627 }
628
629 pub fn new_object(&mut self, class_oop: Oop) -> Result<Oop> {
630 self.activate()?;
631 let oop = unsafe { self.lib.gci_new_oop(class_oop.raw())? };
632 self.check_oop("new_object", oop)?;
633 Ok(Oop(oop))
634 }
635
636 pub fn smallint_oop(&self, value: i64) -> Oop {
637 Oop::from_smallint(value)
638 }
639
640 pub fn bool_oop(&self, value: bool) -> Oop {
641 Oop::from_bool(value)
642 }
643
644 pub fn nil_oop(&self) -> Oop {
645 Oop::NIL
646 }
647
648 pub fn float_oop(&mut self, value: f64) -> Result<Oop> {
649 self.activate()?;
650 let oop = unsafe { self.lib.gci_flt_to_oop(value as c_double)? };
651 if oop == OOP_ILLEGAL || oop == OOP_NIL {
652 return Err(Error::IllegalOop {
653 operation: "float_oop",
654 });
655 }
656 Ok(Oop(oop))
657 }
658
659 pub fn try_oop_to_float(&mut self, oop: Oop) -> Result<Option<f64>> {
660 self.activate()?;
661 let mut value: c_double = 0.0;
662 let ok = unsafe {
663 self.lib
664 .gci_oop_to_flt(oop.raw(), &mut value as *mut c_double)?
665 };
666 Ok((ok != 0).then_some(value as f64))
667 }
668
669 pub fn value_to_oop(&mut self, value: &Value) -> Result<Oop> {
670 match value {
671 Value::Nil => Ok(Oop::NIL),
672 Value::Bool(value) => Ok(Oop::from_bool(*value)),
673 Value::SmallInt(value) => Ok(Oop::from_smallint(*value)),
674 Value::Char(value) => Ok(Oop::from_char(*value)),
675 Value::String(value) => self.new_string(value),
676 Value::Oop(oop) => Ok(*oop),
677 }
678 }
679
680 pub fn identity_for_oop(&self, oop: Oop) -> usize {
681 let mut identity_map = self.identity_map.borrow_mut();
682 if let Some(identity) = identity_map.get(&oop.raw()).copied() {
683 return identity;
684 }
685 let identity = self.next_identity.get();
686 self.next_identity.set(identity.saturating_add(1));
687 identity_map.insert(oop.raw(), identity);
688 identity
689 }
690
691 pub fn cached_identity_for_oop(&self, oop: Oop) -> Option<usize> {
692 self.identity_map.borrow().get(&oop.raw()).copied()
693 }
694
695 pub fn identity_map_len(&self) -> usize {
696 self.identity_map.borrow().len()
697 }
698
699 pub fn clear_identity_map(&self) {
700 self.identity_map.borrow_mut().clear();
701 self.next_identity.set(1);
702 }
703
704 pub fn fetch_size(&mut self, oop: Oop) -> Result<i64> {
705 self.activate()?;
706 Ok(unsafe { self.lib.gci_fetch_size(oop.raw())? })
707 }
708
709 pub fn fetch_string(&mut self, oop: Oop) -> Result<String> {
710 let size = self.fetch_size(oop)?;
711 if size < 0 {
712 return Err(Error::NegativeSize(size));
713 }
714 if size == 0 {
715 return Ok(String::new());
716 }
717
718 let mut buffer = vec![0 as c_char; size as usize + 1];
719 self.activate()?;
720 let fetched = unsafe {
721 self.lib
722 .gci_fetch_bytes(oop.raw(), 1, buffer.as_mut_ptr(), size)?
723 };
724 if fetched < 0 {
725 return Err(Error::NegativeSize(fetched));
726 }
727 let bytes: Vec<u8> = buffer
728 .iter()
729 .take(fetched as usize)
730 .map(|byte| *byte as u8)
731 .collect();
732 Ok(String::from_utf8_lossy(&bytes).into_owned())
733 }
734
735 pub fn fetch_class(&mut self, oop: Oop) -> Result<Oop> {
736 self.activate()?;
737 let class_oop = unsafe { self.lib.gci_fetch_class(oop.raw())? };
738 self.check_oop("fetch_class", class_oop)?;
739 Ok(Oop(class_oop))
740 }
741
742 pub fn array_oops(&mut self, oop: Oop) -> Result<Vec<Oop>> {
743 let size = self.fetch_size(oop)?;
744 if size < 0 {
745 return Err(Error::NegativeSize(size));
746 }
747 let mut values = Vec::with_capacity(size as usize);
748 for index in 1..=size {
749 let index = self.smallint_oop(index);
750 values.push(self.perform_oop(oop, "at:", &[index])?);
751 }
752 Ok(values)
753 }
754
755 pub fn array_strings(&mut self, oop: Oop) -> Result<Vec<String>> {
756 let values = self.array_oops(oop)?;
757 values
758 .into_iter()
759 .map(|value| self.fetch_string(value))
760 .collect()
761 }
762
763 pub fn global_get(&mut self, symbol_name: &str) -> Result<Oop> {
764 let user_globals = self.resolve("UserGlobals")?;
765 let key = CString::new(symbol_name)?;
766 let mut value = OOP_ILLEGAL;
767 let mut assoc = OOP_ILLEGAL;
768 self.activate()?;
769 unsafe {
770 self.lib.gci_sym_dict_at(
771 user_globals.raw(),
772 &key,
773 &mut value as *mut RawOop,
774 &mut assoc as *mut RawOop,
775 )?;
776 }
777 self.check_oop("global_get", value)?;
778 Ok(Oop(value))
779 }
780
781 pub fn global_put(&mut self, symbol_name: &str, value: Oop) -> Result<()> {
782 let user_globals = self.resolve("UserGlobals")?;
783 let key = self.new_symbol(symbol_name)?;
784 self.activate()?;
785 unsafe {
786 self.lib
787 .gci_sym_dict_at_obj_put(user_globals.raw(), key.raw(), value.raw())?;
788 }
789 Ok(())
790 }
791
792 pub fn str_dict_get(&mut self, dict: Oop, key: &str) -> Result<Value> {
793 let key = CString::new(key)?;
794 let mut value = OOP_ILLEGAL;
795 self.activate()?;
796 unsafe {
797 self.lib
798 .gci_str_key_value_dict_at(dict.raw(), &key, &mut value as *mut RawOop)?;
799 }
800 self.check_oop("str_dict_get", value)?;
801 self.marshal(Oop(value))
802 }
803
804 pub fn str_dict_put(&mut self, dict: Oop, key: &str, value: Oop) -> Result<()> {
805 let key = CString::new(key)?;
806 self.activate()?;
807 unsafe {
808 self.lib
809 .gci_str_key_value_dict_at_put(dict.raw(), &key, value.raw())?;
810 }
811 Ok(())
812 }
813
814 pub fn retain_oop(&mut self, oop: Oop) -> Result<OopHandle<'_>> {
815 self.add_to_export_set(oop)?;
816 Ok(OopHandle {
817 session: self,
818 oop,
819 released: false,
820 })
821 }
822
823 pub fn add_to_export_set(&mut self, oop: Oop) -> Result<()> {
824 self.activate()?;
825 unsafe {
826 if !self
827 .lib
828 .call_optional_oop_export(b"GciAddOopToExportSet", oop.raw())?
829 {
830 self.lib
831 .call_optional_oop_export(b"GciAddObjToExportSet", oop.raw())?;
832 }
833 }
834 Ok(())
835 }
836
837 pub fn remove_from_export_set(&mut self, oop: Oop) -> Result<()> {
838 if !self.logged_in.get() {
839 return Ok(());
840 }
841 self.activate()?;
842 unsafe {
843 if !self
844 .lib
845 .call_optional_oop_export(b"GciRemoveOopFromExportSet", oop.raw())?
846 {
847 self.lib
848 .call_optional_oop_export(b"GciRemoveObjFromExportSet", oop.raw())?;
849 }
850 }
851 Ok(())
852 }
853
854 pub fn needs_commit(&mut self) -> Result<bool> {
855 self.activate()?;
856 Ok(unsafe { self.lib.gci_needs_commit()? != 0 })
857 }
858
859 pub fn in_transaction(&mut self) -> Result<bool> {
860 self.activate()?;
861 Ok(unsafe { self.lib.gci_in_transaction()? != 0 })
862 }
863
864 pub fn commit(&mut self) -> Result<()> {
865 self.activate()?;
866 let mut err = GciErrSType::default();
867 let ok = unsafe { self.lib.gci_commit(&mut err)? };
868 if ok == 0 && err.number != 0 {
869 return Err(gemstone_error(err));
870 }
871 Ok(())
872 }
873
874 pub fn abort(&mut self) -> Result<()> {
875 self.activate()?;
876 let mut err = GciErrSType::default();
877 let ok = unsafe { self.lib.gci_abort(&mut err)? };
878 if ok == 0 && err.number != 0 {
879 return Err(gemstone_error(err));
880 }
881 Ok(())
882 }
883
884 pub fn transaction<T>(&mut self, body: impl FnOnce(&mut Session) -> Result<T>) -> Result<T> {
885 match body(self) {
886 Ok(value) => {
887 self.commit()?;
888 Ok(value)
889 }
890 Err(err) => {
891 let _ = self.abort();
892 Err(err)
893 }
894 }
895 }
896
897 pub fn commit_with_retry(&mut self, retries: usize) -> Result<()> {
898 let mut attempts = 0;
899 loop {
900 match self.commit() {
901 Ok(()) => return Ok(()),
902 Err(err) if attempts < retries => {
903 attempts += 1;
904 let _ = self.abort();
905 if err_is_fatal(&err) {
906 return Err(err);
907 }
908 }
909 Err(err) => return Err(err),
910 }
911 }
912 }
913
914 pub fn bridge_root(&mut self) -> Result<BridgeRoot<'_>> {
915 BridgeRoot::new(self)
916 }
917
918 pub fn bridge_root_named(&mut self, name: impl Into<String>) -> Result<BridgeRoot<'_>> {
919 BridgeRoot::named(self, name)
920 }
921
922 pub fn logout(&mut self) -> Result<()> {
923 if !self.logged_in.get() {
924 return Ok(());
925 }
926 unsafe {
927 self.lib.gci_set_session_id(self.session_id)?;
928 self.lib.gci_logout()?;
929 }
930 self.logged_in.set(false);
931 self.session_id = GCI_INVALID_SESSION as i32;
932 Ok(())
933 }
934
935 fn open(&mut self, config: Config) -> Result<()> {
936 unsafe {
937 self.lib.gci_init()?;
938 }
939
940 let stone_nrs = CString::new(config.stone_nrs())?;
941 let host_username = CString::new(config.host_username)?;
942 let host_password = CString::new(config.host_password)?;
943 let gem_service = CString::new(config.gem_service)?;
944 let username = CString::new(config.username)?;
945 let password = CString::new(config.password)?;
946
947 let mut encrypted_host_password = vec![0 as c_char; GCI_ENCRYPT_BUF_SIZE];
948 unsafe {
949 self.lib.gci_encrypt(
950 &host_password,
951 encrypted_host_password.as_mut_ptr(),
952 GCI_ENCRYPT_BUF_SIZE as c_uint,
953 )?;
954 self.lib.gci_set_net(
955 &stone_nrs,
956 &host_username,
957 encrypted_host_password.as_ptr(),
958 &gem_service,
959 )?;
960 let ok = self.lib.gci_login_ex(&username, &password, 0, 0)?;
961 if ok == 0 {
962 let mut err = GciErrSType::default();
963 self.lib.gci_err(&mut err)?;
964 return Err(gemstone_error(err));
965 }
966 self.session_id = self.lib.gci_get_session_id()?;
967 }
968 self.logged_in.set(true);
969 Ok(())
970 }
971
972 fn activate(&self) -> Result<()> {
973 if !self.logged_in.get() {
974 return Err(Error::NotLoggedIn);
975 }
976 unsafe {
977 self.lib.gci_set_session_id(self.session_id)?;
978 }
979 Ok(())
980 }
981
982 fn marshal(&self, oop: Oop) -> Result<Value> {
983 match oop.raw() {
984 OOP_NIL => Ok(Value::Nil),
985 OOP_TRUE => Ok(Value::Bool(true)),
986 OOP_FALSE => Ok(Value::Bool(false)),
987 raw if is_smallint(raw) => Ok(Value::SmallInt(gemstone_gci::smallint_to_i64(raw))),
988 raw if is_char(raw) => Ok(Value::Char(char_from_oop(raw)?)),
989 _ => Ok(Value::Oop(oop)),
990 }
991 }
992
993 fn check_oop(&self, operation: &'static str, oop: RawOop) -> Result<()> {
994 if oop == OOP_ILLEGAL {
995 Err(Error::IllegalOop { operation })
996 } else {
997 Ok(())
998 }
999 }
1000}
1001
1002impl Drop for Session {
1003 fn drop(&mut self) {
1004 let _ = self.logout();
1005 }
1006}
1007
1008pub use gemstone_gci::{OOP_FALSE, OOP_ILLEGAL, OOP_NIL, OOP_TRUE};
1009
1010pub struct OopHandle<'a> {
1011 session: &'a mut Session,
1012 oop: Oop,
1013 released: bool,
1014}
1015
1016impl<'a> OopHandle<'a> {
1017 pub fn oop(&self) -> Oop {
1018 self.oop
1019 }
1020
1021 pub fn release(mut self) -> Result<()> {
1022 self.release_now()
1023 }
1024
1025 fn release_now(&mut self) -> Result<()> {
1026 if self.released {
1027 return Ok(());
1028 }
1029 self.session.remove_from_export_set(self.oop)?;
1030 self.released = true;
1031 Ok(())
1032 }
1033}
1034
1035impl Drop for OopHandle<'_> {
1036 fn drop(&mut self) {
1037 let _ = self.release_now();
1038 }
1039}
1040
1041fn required_env(name: &'static str) -> Result<String> {
1042 env::var(name)
1043 .ok()
1044 .filter(|value| !value.is_empty())
1045 .ok_or(Error::MissingEnvironment(name))
1046}
1047
1048fn required_config(name: &'static str, value: Option<String>) -> Result<String> {
1049 value
1050 .filter(|value| !value.is_empty())
1051 .ok_or(Error::MissingConfig(name))
1052}
1053
1054fn gemstone_error(err: GciErrSType) -> Error {
1055 Error::GemStone {
1056 number: err.number,
1057 fatal: err.fatal != 0,
1058 message: err.full_message(),
1059 }
1060}
1061
1062fn err_is_fatal(err: &Error) -> bool {
1063 matches!(err, Error::GemStone { fatal: true, .. })
1064}
1065
1066#[cfg(test)]
1067mod tests {
1068 use super::*;
1069 use std::sync::{Mutex, MutexGuard};
1070
1071 static LIVE_TEST_LOCK: Mutex<()> = Mutex::new(());
1072
1073 #[test]
1074 fn config_formats_remote_stone_nrs() {
1075 let config = Config {
1076 stone: "seaside".to_string(),
1077 netldi: "netldi".to_string(),
1078 host: "db.example.test".to_string(),
1079 username: "DataCurator".to_string(),
1080 password: "swordfish".to_string(),
1081 ..Config::default()
1082 };
1083
1084 assert_eq!(config.stone_nrs(), "!@db.example.test!netldi!seaside");
1085 }
1086
1087 #[test]
1088 fn config_builder_applies_defaults_and_required_fields() -> Result<()> {
1089 let config = Config::builder()
1090 .username("DataCurator")
1091 .password("swordfish")
1092 .stone("seaside")
1093 .build()?;
1094
1095 assert_eq!(config.stone, "seaside");
1096 assert_eq!(config.host, "localhost");
1097 assert_eq!(config.netldi, "netldi");
1098 assert_eq!(config.gem_service, "gemnetobject");
1099 Ok(())
1100 }
1101
1102 #[test]
1103 fn config_builder_requires_credentials() {
1104 let err = Config::builder().password("swordfish").build().unwrap_err();
1105 assert!(matches!(err, Error::MissingConfig("username")));
1106 }
1107
1108 #[test]
1109 fn smallint_values_marshal_without_a_live_session() {
1110 let value = Value::SmallInt(gemstone_gci::smallint_to_i64(
1111 gemstone_gci::i64_to_smallint(7),
1112 ));
1113 assert_eq!(value, Value::SmallInt(7));
1114 }
1115
1116 #[test]
1117 fn value_oop_accessors_are_explicit() {
1118 let oop = Oop::from_smallint(7);
1119 assert_eq!(Value::Oop(oop).as_oop(), Some(oop));
1120 assert_eq!(Value::SmallInt(7).as_oop(), None);
1121 assert_eq!(Oop::from_bool(true).as_bool(), Some(true));
1122 assert_eq!(Oop::from_char('A').as_char().unwrap(), Some('A'));
1123 }
1124
1125 #[test]
1126 fn bridge_values_are_plain_rust_data_until_mapped() {
1127 let value = BridgeValue::dictionary([
1128 ("name".to_string(), BridgeValue::from("Tariq")),
1129 ("amount".to_string(), BridgeValue::from(100_i64)),
1130 ("currency".to_string(), BridgeValue::from("GBP")),
1131 ]);
1132 let BridgeValue::Dictionary(entries) = value else {
1133 panic!("expected dictionary bridge value");
1134 };
1135 assert_eq!(entries["name"], BridgeValue::String("Tariq".to_string()));
1136 assert_eq!(entries["amount"], BridgeValue::SmallInt(100));
1137 }
1138
1139 #[derive(Debug, Eq, PartialEq)]
1140 struct TestBookingDraft {
1141 name: String,
1142 amount: i64,
1143 currency: String,
1144 }
1145
1146 impl BridgeMapped for TestBookingDraft {
1147 fn to_bridge_value(&self) -> BridgeValue {
1148 BridgeValue::dictionary([
1149 ("name".to_string(), BridgeValue::from(self.name.clone())),
1150 ("amount".to_string(), BridgeValue::from(self.amount)),
1151 (
1152 "currency".to_string(),
1153 BridgeValue::from(self.currency.clone()),
1154 ),
1155 ])
1156 }
1157
1158 fn from_bridge_dictionary(dictionary: &mut BridgeDictionary<'_>) -> Result<Self> {
1159 Ok(Self {
1160 name: dictionary.at_string("name")?,
1161 amount: dictionary.at_smallint("amount")?,
1162 currency: dictionary.at_string("currency")?,
1163 })
1164 }
1165 }
1166
1167 #[derive(Clone, Debug, Eq, PartialEq, BridgeMapped)]
1168 struct DeriveCustomerDraft {
1169 #[bridge(key_type = "Symbol")]
1170 name: String,
1171 }
1172
1173 #[derive(Clone, Debug, Eq, PartialEq, BridgeMapped)]
1174 struct DeriveBookingDraft {
1175 #[bridge(key = "amount", key_type = "Symbol")]
1176 amount: i64,
1177 customer: DeriveCustomerDraft,
1178 tags: Vec<String>,
1179 }
1180
1181 #[test]
1182 fn bridge_mapped_derive_supports_key_policy_and_nested_values() {
1183 let value = DeriveBookingDraft {
1184 amount: 100,
1185 customer: DeriveCustomerDraft {
1186 name: "Tariq".to_string(),
1187 },
1188 tags: vec!["priority".to_string(), "demo".to_string()],
1189 }
1190 .to_bridge_value();
1191
1192 let BridgeValue::KeyedDictionary(entries) = value else {
1193 panic!("expected keyed dictionary");
1194 };
1195 assert_eq!(entries[0].0.name, "amount");
1196 assert_eq!(entries[0].0.key_type, BridgeKeyType::Symbol);
1197 assert!(matches!(entries[1].1, BridgeValue::KeyedDictionary(_)));
1198 assert!(matches!(entries[2].1, BridgeValue::Array(_)));
1199 }
1200
1201 #[test]
1202 fn live_eval_smoke_returns_seven_when_enabled() -> Result<()> {
1203 let Some(_guard) = live_test_guard() else {
1204 return Ok(());
1205 };
1206
1207 let mut session = Session::login(Config::from_env()?)?;
1208 assert_eq!(session.eval("3 + 4")?, Value::SmallInt(7));
1209 session.logout()?;
1210 Ok(())
1211 }
1212
1213 #[test]
1214 fn live_login_logout_when_enabled() -> Result<()> {
1215 let Some(_guard) = live_test_guard() else {
1216 return Ok(());
1217 };
1218 let Some(mut session) = live_session()? else {
1219 return Ok(());
1220 };
1221
1222 assert!(session.is_logged_in());
1223 session.logout()?;
1224 assert!(!session.is_logged_in());
1225 Ok(())
1226 }
1227
1228 #[test]
1229 fn live_perform_when_enabled() -> Result<()> {
1230 let Some(_guard) = live_test_guard() else {
1231 return Ok(());
1232 };
1233 let Some(mut session) = live_session()? else {
1234 return Ok(());
1235 };
1236
1237 let seven = session.smallint_oop(7);
1238 let printed = session.perform_oop(seven, "printString", &[])?;
1239 assert_eq!(session.fetch_string(printed)?, "7");
1240 Ok(())
1241 }
1242
1243 #[test]
1244 fn live_global_string_round_trip_when_enabled() -> Result<()> {
1245 let Some(_guard) = live_test_guard() else {
1246 return Ok(());
1247 };
1248 let Some(mut session) = live_session()? else {
1249 return Ok(());
1250 };
1251
1252 let key = live_key("GemStoneRsRoundTrip");
1253 let text = session.new_string("hello from gemstone-rs")?;
1254 session.global_put(&key, text)?;
1255 let stored = session.global_get(&key)?;
1256 assert_eq!(session.fetch_string(stored)?, "hello from gemstone-rs");
1257 session.global_put(&key, Oop::NIL)?;
1258 session.commit()?;
1259 Ok(())
1260 }
1261
1262 #[test]
1263 fn live_error_handling_when_enabled() -> Result<()> {
1264 let Some(_guard) = live_test_guard() else {
1265 return Ok(());
1266 };
1267 let Some(mut session) = live_session()? else {
1268 return Ok(());
1269 };
1270
1271 session.logout()?;
1272 let err = session.eval("3 + 4").unwrap_err();
1273 assert!(matches!(err, Error::NotLoggedIn));
1274 Ok(())
1275 }
1276
1277 #[test]
1278 fn live_transaction_commit_and_abort_when_enabled() -> Result<()> {
1279 let Some(_guard) = live_test_guard() else {
1280 return Ok(());
1281 };
1282 let Some(mut session) = live_session()? else {
1283 return Ok(());
1284 };
1285
1286 let committed_key = live_key("GemStoneRsCommitted");
1287 session.transaction(|session| {
1288 let value = session.new_string("committed")?;
1289 session.global_put(&committed_key, value)
1290 })?;
1291 let committed = session.global_get(&committed_key)?;
1292 assert_eq!(session.fetch_string(committed)?, "committed");
1293
1294 let aborted_key = live_key("GemStoneRsAborted");
1295 let aborted_value = session.new_string("aborted")?;
1296 session.global_put(&aborted_key, aborted_value)?;
1297 session.abort()?;
1298 assert!(session.global_get(&aborted_key).is_err());
1299
1300 session.global_put(&committed_key, Oop::NIL)?;
1301 session.commit()?;
1302 Ok(())
1303 }
1304
1305 #[test]
1306 fn live_bridge_root_mapping_when_enabled() -> Result<()> {
1307 let Some(_guard) = live_test_guard() else {
1308 return Ok(());
1309 };
1310 let Some(mut session) = live_session()? else {
1311 return Ok(());
1312 };
1313
1314 let key = live_key("GemStoneRsBridgePayload");
1315 let payload = TestBookingDraft {
1316 name: "Tariq".to_string(),
1317 amount: 100,
1318 currency: "GBP".to_string(),
1319 };
1320 let mut bridge_root = session.bridge_root()?;
1321 let payload_oop = bridge_root.put_mapped(&key, &payload)?;
1322 let stored = bridge_root.get_oop(&key)?;
1323 assert_eq!(payload_oop, stored);
1324 let loaded: TestBookingDraft = bridge_root.get_mapped(&key)?;
1325 assert_eq!(loaded, payload);
1326 bridge_root.remove(&key)?;
1327 bridge_root.commit()?;
1328 Ok(())
1329 }
1330
1331 fn live_session() -> Result<Option<Session>> {
1332 if env::var("GS_RUN_LIVE_RUST").as_deref() != Ok("1") {
1333 return Ok(None);
1334 }
1335 Session::login(Config::from_env()?).map(Some)
1336 }
1337
1338 fn live_test_guard() -> Option<MutexGuard<'static, ()>> {
1339 if env::var("GS_RUN_LIVE_RUST").as_deref() == Ok("1") {
1340 Some(
1341 LIVE_TEST_LOCK
1342 .lock()
1343 .unwrap_or_else(|poisoned| poisoned.into_inner()),
1344 )
1345 } else {
1346 None
1347 }
1348 }
1349
1350 fn live_key(prefix: &str) -> String {
1351 let nanos = std::time::SystemTime::now()
1352 .duration_since(std::time::UNIX_EPOCH)
1353 .map(|value| value.as_nanos())
1354 .unwrap_or_default();
1355 format!("{prefix}{nanos}")
1356 }
1357}