# frozen_string_literal: true
# = PStore -- Transactional File Storage for Ruby Objects
# documentation by Kev Jackson and James Edward Gray II
# See PStore for documentation.
# \PStore implements a file based persistence mechanism based on a Hash.
# User code can store hierarchies of Ruby objects (values)
# into the data store by name (keys).
# An object hierarchy may be just a single object.
# User code may later read values back from the data store
# or even update data, as needed.
# The transactional behavior ensures that any changes succeed or fail together.
# This can be used to ensure that the data store is not left in a transitory state,
# where some values were updated but others were not.
# Behind the scenes, Ruby objects are stored to the data store file with Marshal.
# That carries the usual limitations. Proc objects cannot be marshalled,
# There are three important concepts here (details at the links):
# - {Store}[rdoc-ref:PStore@The+Store]: a store is an instance of \PStore.
# - {Entries}[rdoc-ref:PStore@Entries]: the store is hash-like;
# each entry is the key for a stored object.
# - {Transactions}[rdoc-ref:PStore@Transactions]: each transaction is a collection
# of prospective changes to the store;
# a transaction is defined in the block given with a call
# Examples on this page need a store that has known properties.
# They can get a new (and populated) store by calling thus:
# example_store do |store|
# # Example code using store goes here.
# All we really need to know about +example_store+
# is that it yields a fresh store with a known population of entries;
# # Yield a pristine store for use in examples.
# # Create the store in a temporary file.
# Tempfile.create do |file|
# store = PStore.new(file)
# The contents of the store are maintained in a file whose path is specified
# when the store is created (see PStore.new).
# The objects are stored and retrieved using
# module Marshal, which means that certain objects cannot be added to the store;
# see {Marshal::dump}[rdoc-ref:Marshal.dump].
# A store may have any number of entries.
# Each entry has a key and a value, just as in a hash:
# - Key: as in a hash, the key can be (almost) any object;
# see {Hash Keys}[rdoc-ref:Hash@Hash+Keys].
# You may find it convenient to keep it simple by using only
# symbols or strings as keys.
# - Value: the value may be any object that can be marshalled by \Marshal
# (see {Marshal::dump}[rdoc-ref:Marshal.dump])
# and in fact may be a collection
# (e.g., an array, a hash, a set, a range, etc).
# That collection may in turn contain nested objects,
# including collections, to any depth;
# those objects must also be \Marshal-able.
# See {Hierarchical Values}[rdoc-ref:PStore@Hierarchical+Values].
# === The Transaction Block
# The block given with a call to method #transaction#
# contains a _transaction_,
# which consists of calls to \PStore methods that
# read from or write to the store
# (that is, all \PStore methods except #transaction itself,
# #path, and Pstore.new):
# example_store do |store|
# store.keys # => [:foo, :bar, :baz]
# store.keys # => [:foo, :bar, :baz, :bat]
# Execution of the transaction is deferred until the block exits,
# and is executed _atomically_ (all-or-nothing):
# either all transaction calls are executed, or none are.
# This maintains the integrity of the store.
# Other code in the block (including even calls to #path and PStore.new)
# is executed immediately, not deferred.
# - May not contain a nested call to #transaction.
# - Is the only context where methods that read from or write to
# As seen above, changes in a transaction are made automatically
# The block may be exited early by calling method #commit or #abort.
# - Method #commit triggers the update to the store and exits the block:
# example_store do |store|
# store.keys # => [:foo, :bar, :baz]
# # Update was completed.
# store.keys # => [:foo, :bar, :baz, :bat]
# - Method #abort discards the update to the store and exits the block:
# example_store do |store|
# store.keys # => [:foo, :bar, :baz]
# # Update was not completed.
# store.keys # => [:foo, :bar, :baz]
# === Read-Only Transactions
# By default, a transaction allows both reading from and writing to
# # Read-write transaction.
# # Any code except a call to #transaction is allowed here.
# If argument +read_only+ is passed as +true+,
# only reading is allowed:
# store.transaction(true) do
# # Read-only transaction:
# # Calls to #transaction, #[]=, and #delete are not allowed here.
# The value for an entry may be a simple object (as seen above).
# It may also be a hierarchy of objects nested to any depth:
# deep_store = PStore.new('deep.store')
# deep_store.transaction do
# array_of_hashes = [{}, {}, {}]
# deep_store[:array_of_hashes] = array_of_hashes
# deep_store[:array_of_hashes] # => [{}, {}, {}]
# hash_of_arrays = {foo: [], bar: [], baz: []}
# deep_store[:hash_of_arrays] = hash_of_arrays
# deep_store[:hash_of_arrays] # => {:foo=>[], :bar=>[], :baz=>[]}
# deep_store[:hash_of_arrays][:foo].push(:bat)
# deep_store[:hash_of_arrays] # => {:foo=>[:bat], :bar=>[], :baz=>[]}
# And recall that you can use
# {dig methods}[rdoc-ref:dig_methods.rdoc]
# in a returned hierarchy of objects.
# == Working with the Store
# Use method PStore.new to create a store.
# The new store creates or opens its containing file:
# store = PStore.new('t.store')
# === Modifying the Store
# Use method #[]= to update or create an entry:
# example_store do |store|
# store[:foo] = 1 # Update.
# store[:bam] = 1 # Create.
# Use method #delete to remove an entry:
# example_store do |store|
# Use method #fetch (allows default) or #[] (defaults to +nil+)
# example_store do |store|
# store.fetch(:baz) # => 2
# store.fetch(:nope, nil) # => nil
# store.fetch(:nope) # Raises exception.
# Use method #key? to determine whether a given key exists:
# example_store do |store|
# store.key?(:foo) # => true
# Use method #keys to retrieve keys:
# example_store do |store|
# store.keys # => [:foo, :bar, :baz]
# Use method #path to retrieve the path to the store's underlying file;
# this method may be called from outside a transaction block:
# store = PStore.new('t.store')
# store.path # => "t.store"
# For transaction safety, see:
# - Optional argument +thread_safe+ at method PStore.new.
# - Attribute #ultra_safe.
# Needless to say, if you're storing valuable data with \PStore, then you should
# backup the \PStore file from time to time.
# def initialize(page_name, author, contents)
# add_revision(author, contents)
# def add_revision(author, contents)
# @revisions << {created: Time.now,
# def wiki_page_references
# [@page_name] + @revisions.last[:contents].scan(/\b(?:[A-Z]+[a-z]+){2,}/)
# # Create a new wiki page.
# home_page = WikiPage.new("HomePage", "James Edward Gray II",
# "A page about the JoysOfDocumentation..." )
# wiki = PStore.new("wiki_pages.pstore")
# # Update page data and the index together, or not at all.
# wiki[home_page.page_name] = home_page
# wiki[:wiki_index] ||= Array.new
# wiki[:wiki_index].push(*home_page.wiki_page_references)
# # Read wiki data, setting argument read_only to true.
# wiki.transaction(true) do
# wiki.keys.each do |key|
RDWR_ACCESS = {mode: IO::RDWR | IO::CREAT | IO::BINARY, encoding: Encoding::ASCII_8BIT}.freeze
RD_ACCESS = {mode: IO::RDONLY | IO::BINARY, encoding: Encoding::ASCII_8BIT}.freeze
WR_ACCESS = {mode: IO::WRONLY | IO::CREAT | IO::TRUNC | IO::BINARY, encoding: Encoding::ASCII_8BIT}.freeze
# The error type thrown by all PStore methods.
class Error < StandardError
# Whether \PStore should do its best to prevent file corruptions,
# even when an unlikely error (such as memory-error or filesystem error) occurs:
# - +true+: changes are posted by creating a temporary file,
# writing the updated data to it, then renaming the file to the given #path.
# File integrity is maintained.
# Note: has effect only if the filesystem has atomic file rename
# (as do POSIX platforms Linux, MacOS, FreeBSD and others).
# - +false+ (the default): changes are posted by rewinding the open file
# and writing the updated data.
# File integrity is maintained if the filesystem raises
# no unexpected I/O error;
# if such an error occurs during a write to the store,
# the file may become corrupted.
attr_accessor :ultra_safe
# Returns a new \PStore object.
# Argument +file+ is the path to the file in which objects are to be stored;
# if the file exists, it should be one that was written by \PStore.
# store = PStore.new(path)
# {reentrant}[https://en.wikipedia.org/wiki/Reentrancy_(computing)].
# If argument +thread_safe+ is given as +true+,
# the object is also thread-safe (at the cost of a small performance penalty):
# store = PStore.new(path, true)
def initialize(file, thread_safe = false)
dir = File::dirname(file)
unless File::directory? dir
raise PStore::Error, format("directory %s does not exist", dir)
if File::exist? file and not File::readable? file
raise PStore::Error, format("file %s not readable", file)
@thread_safe = thread_safe
@lock = Thread::Mutex.new
# Raises PStore::Error if the calling code is not in a PStore#transaction.
raise PStore::Error, "not in transaction" unless @lock.locked?
# Raises PStore::Error if the calling code is not in a PStore#transaction or
# if the code is in a read-only PStore#transaction.
raise PStore::Error, "in read-only transaction" if @rdonly
private :in_transaction, :in_transaction_wr
# Returns the value for the given +key+ if the key exists.
# if not +nil+, the returned value is an object or a hierarchy of objects:
# example_store do |store|
# Returns +nil+ if there is no such key.
# See also {Hierarchical Values}[rdoc-ref:PStore@Hierarchical+Values].
# Raises an exception if called outside a transaction block.
# Like #[], except that it accepts a default value for the store.
# If the +key+ does not exist:
# - Raises an exception if +default+ is +PStore::Error+.
# - Returns the value of +default+ otherwise:
# example_store do |store|
# store.fetch(:nope, nil) # => nil
# store.fetch(:nope) # Raises an exception.
# Raises an exception if called outside a transaction block.
def fetch(key, default=PStore::Error)
if default == PStore::Error
raise PStore::Error, format("undefined key `%s'", key)
# Creates or replaces the value for the given +key+:
# example_store do |store|
# See also {Hierarchical Values}[rdoc-ref:PStore@Hierarchical+Values].
# Raises an exception if called outside a transaction block.
# Removes and returns the value at +key+ if it exists:
# example_store do |store|
# Returns +nil+ if there is no such key.
# Raises an exception if called outside a transaction block.
# Returns an array of the existing keys:
# example_store do |store|
# store.keys # => [:foo, :bar, :baz]
# Raises an exception if called outside a transaction block.
# PStore#roots is an alias for PStore#keys.
# Returns +true+ if +key+ exists, +false+ otherwise:
# example_store do |store|