summaryrefslogtreecommitdiff
path: root/lib/pstore.rb
diff options
context:
space:
mode:
authorBurdetteLamar <[email protected]>2022-06-27 13:16:58 -0500
committergit <[email protected]>2022-07-02 21:49:07 +0900
commit8715ecd04b8bb4976b89913be4e790e5d15c4b74 (patch)
tree28e15df320c5a2b5270f1d05a9c791f68e0ab4e9 /lib/pstore.rb
parent7b78aba53aa0b34800bfd96e9e278258d2a890c8 (diff)
[ruby/pstore] Enhanced RDoc
https://github.com/ruby/pstore/commit/81a266d88c
Diffstat (limited to 'lib/pstore.rb')
-rw-r--r--lib/pstore.rb497
1 files changed, 343 insertions, 154 deletions
diff --git a/lib/pstore.rb b/lib/pstore.rb
index a46bcb84bc..177f1b2dfc 100644
--- a/lib/pstore.rb
+++ b/lib/pstore.rb
@@ -10,87 +10,268 @@
require "digest"
+# An instance of class \PStore can store and retrieve Ruby objects --
+# not just strings or raw data, but objects of many kinds.
+# There are three key terms here (details at the links):
#
-# PStore implements a file based persistence mechanism based on a Hash. User
-# code can store hierarchies of Ruby objects (values) into the data store file
-# 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.
+# - {Store}[rdoc-ref:PStore@The+Store]: a store is an instance of \PStore.
+# - {Roots}[rdoc-ref:PStore@Roots]: the store is hash-like;
+# each root is a key for a stored object.
+# - {Transactions}[rdoc-ref:PStore@Transactions]: each transaction is a ollection
+# of prospective changes to the store;
+# a transaction is defined in the block given with a call
+# to PStore#transaction.
#
-# 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.
+# == About the Examples
#
-# Behind the scenes, Ruby objects are stored to the data store file with
-# Marshal. That carries the usual limitations. Proc objects cannot be
-# marshalled, for example.
+# All examples on this page assume that the following code has been executed:
#
-# == Usage example:
+# require 'pstore'
+# # Create a store with file +flat.store+.
+# store = PStore.new('flat.store')
+# # Store some objects.
+# store.transaction do
+# store[:foo] = 0
+# store[:bar] = 1
+# store[:baz] = 2
+# end
+#
+# To avoid modifying the example store, some examples first execute
+# <tt>temp = store.dup</tt>, then apply changes to +temp+
+#
+# == The Store
+#
+# The contents of the store are maintained in a file whose path is specified
+# when the store is created (see PStore.new):
+#
+# - Ruby objects put into the store are serialized as string data
+# and written to the file;
+# - Data retrieved from the store is read from the file and deserialized
+# to form Ruby objects.
+#
+# The objects are serialized and deserialized using
+# module Marshal, which means that certain objects cannot be added to the store;
+# see {Marshal::dump}[https://docs.ruby-lang.org/en/master/Marshal.html#method-c-dump].
+#
+# == Roots
+#
+# A store may have any number of entries, called _roots_.
+# Each root 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}[https://docs.ruby-lang.org/en/master/Hash.html].
+# You may find it convenient to keep it simple by using only
+# symbols or strings as keys.
+# - Value: the value truly may be any object, and in fact can be a collection
+# (e.g., an array, a hash, a set, a range, etc).
+# That collection may in turn contain nested collections, to any depth.
+# See {Deep Root Values}[rdoc-ref:PStore@Deep+Root+Values].
+#
+# == Transactions
+#
+# A call to PStore#transaction must have a block.
+#
+# A transaction consists of just those \PStore method calls in the block
+# that would modify the store; those methods are #[]= and #delete.
+# Note that the block may contain any code whatsoever
+# except a nested call to #transaction.
+#
+# An instance method in \PStore may be called only from within a transaction
+# (with the exception the #path may be called from anywhere).
+# This assures that the call is executed only when the store is secure and stable.
+#
+# When the transaction block exits,
+# the specified changes are made automatically.
+# (and atomically; that is, either all changes are posted, or none are).
+#
+# Exactly how the changes are posted
+# depends on the value of attribute #ultra_safe (details at the link).
+#
+# The block may be exited early by calling method #commit or #abort.
+#
+# - Method #commit triggers the update to the store and exits the block:
+#
+# temp = store.dup
+# temp.transaction do
+# temp.roots # => [:foo, :bar, :baz]
+# temp[:bat] = 3
+# temp.commit
+# fail 'Cannot get here'
+# end
+# temp.transaction do
+# # Update was completed.
+# store.roots # => [:foo, :bar, :baz, :bat]
+# end
+#
+# - Method #abort discards the update to the store and exits the block:
+#
+# store.transaction do
+# store[:bam] = 4
+# store.abort
+# fail 'Cannot get here'
+# end
+# store.transaction do
+# # Update was not completed.
+# store[:bam] # => nil
+# end
+#
+# Each transaction is either:
+#
+# - Read-write (the default):
+#
+# store.transaction do
+# # Read-write transaction.
+# # Any code except a call to #transaction is allowed here.
+# end
+#
+# - Read-only (optional argument +read_only+ set to +true+):
+#
+# store.transaction(true) do
+# # Read-only transaction:
+# # Calls to #transaction, #[]=, and #delete are not allowed here.
+# end
+#
+# == Deep Root Values
+#
+# The value for a root 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=>[]}
+# end
+#
+# And recall that you can use
+# {dig methods}[https://docs.ruby-lang.org/en/master/dig_methods_rdoc.html]
+# in a returned hierarchy of objects.
+#
+# == Working with the Store
+#
+# === Creating a 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 a root:
+#
+# temp = store.dup
+# temp.transaction do
+# temp[:foo] = 1 # Update.
+# temp[:bam] = 1 # Create.
+# end
+#
+# Use method #delete to remove a root:
+#
+# temp = store.dup
+# temp.transaction do
+# temp.delete(:foo)
+# temp[:foo] # => nil
+# end
+#
+# === Retrieving Stored Objects
+#
+# Use method #fetch (allows default) or #[] (defaults to +nil+)
+# to retrieve a root:
+#
+# store.transaction do
+# store[:foo] # => 0
+# store[:nope] # => nil
+# store.fetch(:baz) # => 2
+# store.fetch(:nope, nil) # => nil
+# store.fetch(:nope) # Raises exception.
+# end
+#
+# === Querying the Store
+#
+# Use method #root? to determine whether a given root exists:
+#
+# store.transaction do
+# store.root?(:foo) # => true.
+# end
+#
+# Use method #roots to retrieve root keys:
+#
+# store.transaction do
+# store.roots # => [:foo, :bar, :baz].
+# end
+#
+# Use method #path to retrieve the path to the store's underlying file:
+#
+# store.transaction do
+# store.path # => "flat.store"
+# end
+#
+# == Transaction Safety
+#
+# 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.
+#
+# == An Example Store
#
# require "pstore"
#
-# # a mock wiki object...
+# # A mock wiki object.
# class WikiPage
-# def initialize( page_name, author, contents )
+#
+# attr_reader :page_name
+#
+# def initialize(page_name, author, contents)
# @page_name = page_name
# @revisions = Array.new
-#
# add_revision(author, contents)
# end
#
-# attr_reader :page_name
-#
-# def add_revision( author, contents )
-# @revisions << { :created => Time.now,
-# :author => author,
-# :contents => contents }
+# def add_revision(author, contents)
+# @revisions << {created: Time.now,
+# author: author,
+# contents: contents}
# end
#
# def wiki_page_references
# [@page_name] + @revisions.last[:contents].scan(/\b(?:[A-Z]+[a-z]+){2,}/)
# end
#
-# # ...
# end
#
-# # create a new page...
-# home_page = WikiPage.new( "HomePage", "James Edward Gray II",
-# "A page about the JoysOfDocumentation..." )
+# # Create a new wiki page.
+# home_page = WikiPage.new("HomePage", "James Edward Gray II",
+# "A page about the JoysOfDocumentation..." )
#
-# # then we want to update page data and the index together, or not at all...
# wiki = PStore.new("wiki_pages.pstore")
-# wiki.transaction do # begin transaction; do all of this or none of it
-# # store page...
+# # Update page data and the index together, or not at all.
+# wiki.transaction do
+# # Store page.
# wiki[home_page.page_name] = home_page
-# # ensure that an index has been created...
+# # Create page index.
# wiki[:wiki_index] ||= Array.new
-# # update wiki index...
+# # Update wiki index.
# wiki[:wiki_index].push(*home_page.wiki_page_references)
-# end # commit changes to wiki data store file
-#
-# ### Some time later... ###
+# end
#
-# # read wiki data...
-# wiki.transaction(true) do # begin read-only transaction, no changes allowed
-# wiki.roots.each do |data_root_name|
-# p data_root_name
-# p wiki[data_root_name]
+# # Read wiki data, setting argument read_only to true.
+# wiki.transaction(true) do
+# wiki.roots.each do |root|
+# puts root
+# puts wiki[root]
# end
# end
#
-# == Transaction modes
-#
-# By default, file integrity is only ensured as long as the operating system
-# (and the underlying hardware) doesn't raise any unexpected I/O errors. If an
-# I/O error occurs while PStore is writing to its file, then the file will
-# become corrupted.
-#
-# You can prevent this by setting <em>pstore.ultra_safe = true</em>.
-# However, this results in a minor performance loss, and only works on platforms
-# that support atomic file renames. Please consult the documentation for
-# +ultra_safe+ for details.
-#
-# Needless to say, if you're storing valuable data with PStore, then you should
-# backup the PStore files from time to time.
class PStore
VERSION = "0.1.1"
@@ -102,21 +283,38 @@ class PStore
class Error < StandardError
end
- # Whether PStore should do its best to prevent file corruptions, even when under
- # unlikely-to-occur error conditions such as out-of-space conditions and other
- # unusual OS filesystem errors. Setting this flag comes at the price in the form
- # of a performance loss.
+ # 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.
#
- # This flag only has effect on platforms on which file renames are atomic (e.g.
- # all POSIX platforms: Linux, MacOS X, FreeBSD, etc). The default value is false.
attr_accessor :ultra_safe
+ # Returns a new \PStore object.
#
- # To construct a PStore object, pass in the _file_ path where you would like
- # the data to be stored.
+ # Argument +file+ is the path to the file in which objects are to be stored;
+ # if the file exists, it must be in a Marshal-compatible format:
#
- # PStore objects are always reentrant. But if _thread_safe_ is set to true,
- # then it will become thread-safe at the cost of a minor performance hit.
+ # path = 't.store'
+ # store = PStore.new(path)
+ #
+ # A \PStore object is
+ # {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)
@@ -147,27 +345,43 @@ class PStore
end
private :in_transaction, :in_transaction_wr
+ # :call-seq:
+ # pstore[key]
+ #
+ # Returns the deserialized value of the root for the given +key+ if it exists.
+ # +nil+ otherwise;
+ # if not +nil+, the returned value is an object or a hierarchy of objects:
#
- # Retrieves a value from the PStore file data, by _name_. The hierarchy of
- # Ruby objects stored under that root _name_ will be returned.
+ # store.transaction do
+ # store[:foo] # => 0
+ # store[:nope] # => nil
+ # end
#
- # *WARNING*: This method is only valid in a PStore#transaction. It will
- # raise PStore::Error if called at any other time.
+ # Returns +nil+ if there is no such root.
#
+ # See also {Deep Root Values}[rdoc-ref:PStore@Deep+Root+Values].
+ #
+ # Raises an exception if called outside a transaction block.
def [](name)
in_transaction
@table[name]
end
+
+ # :call-seq:
+ # fetch(key)
+ #
+ # Like #[], except that it accepts a default value for the store.
+ # If the root for the given +key+ does not exist:
#
- # This method is just like PStore#[], save that you may also provide a
- # _default_ value for the object. In the event the specified _name_ is not
- # found in the data store, your _default_ will be returned instead. If you do
- # not specify a default, PStore::Error will be raised if the object is not
- # found.
+ # - Raises an exception if +default+ is +PStore::Error+.
+ # - Returns the value of +default+ otherwise:
#
- # *WARNING*: This method is only valid in a PStore#transaction. It will
- # raise PStore::Error if called at any other time.
+ # store.transaction do
+ # store.fetch(:nope, nil) # => nil
+ # store.fetch(:nope) # Raises an exception.
+ # end
#
+ # Raises an exception if called outside a transaction block.
def fetch(name, default=PStore::Error)
in_transaction
unless @table.key? name
@@ -179,137 +393,112 @@ class PStore
end
@table[name]
end
+
+ # :call-seq:
+ # pstore[key] = value
#
- # Stores an individual Ruby object or a hierarchy of Ruby objects in the data
- # store file under the root _name_. Assigning to a _name_ already in the data
- # store clobbers the old data.
- #
- # == Example:
- #
- # require "pstore"
+ # Creates or replaces an object or hierarchy of objects
+ # at the root for +key+:
#
- # store = PStore.new("data_file.pstore")
- # store.transaction do # begin transaction
- # # load some data into the store...
- # store[:single_object] = "My data..."
- # store[:obj_hierarchy] = { "Kev Jackson" => ["rational.rb", "pstore.rb"],
- # "James Gray" => ["erb.rb", "pstore.rb"] }
- # end # commit changes to data store file
+ # store = PStore.new('t.store')
+ # store.transaction do
+ # store[:bat] = 3
+ # end
#
- # *WARNING*: This method is only valid in a PStore#transaction and it cannot
- # be read-only. It will raise PStore::Error if called at any other time.
+ # See also {Deep Root Values}[rdoc-ref:PStore@Deep+Root+Values].
#
+ # Raises an exception if called outside a transaction block.
def []=(name, value)
in_transaction_wr
@table[name] = value
end
+
+ # :call-seq:
+ # delete(key)
+ #
+ # Removes and returns the root for +key+ if it exists:
#
- # Removes an object hierarchy from the data store, by _name_.
+ # store = PStore.new('t.store')
+ # store.transaction do
+ # store[:bat] = 3
+ # store.delete(:bat)
+ # end
#
- # *WARNING*: This method is only valid in a PStore#transaction and it cannot
- # be read-only. It will raise PStore::Error if called at any other time.
+ # Returns +nil+ if there is no such root.
#
+ # Raises an exception if called outside a transaction block.
def delete(name)
in_transaction_wr
@table.delete name
end
+ # Returns an array of the keys of the existing roots:
#
- # Returns the names of all object hierarchies currently in the store.
- #
- # *WARNING*: This method is only valid in a PStore#transaction. It will
- # raise PStore::Error if called at any other time.
+ # store.transaction do
+ # store.roots # => [:foo, :bar, :baz]
+ # end
#
+ # Raises an exception if called outside a transaction block.
def roots
in_transaction
@table.keys
end
+
+ # :call-seq:
+ # root?(key)
#
- # Returns true if the supplied _name_ is currently in the data store.
+ # Returns +true+ if there is a root for +key+, +false+ otherwise:
#
- # *WARNING*: This method is only valid in a PStore#transaction. It will
- # raise PStore::Error if called at any other time.
+ # store.transaction do
+ # store.root?(:foo) # => true
+ # end
#
+ # Raises an exception if called outside a transaction block.
def root?(name)
in_transaction
@table.key? name
end
- # Returns the path to the data store file.
+
+ # Returns the string file path used to create the store:
+ #
+ # store.path # => "flat.store"
+ #
def path
@filename
end
+ # Exits the current transaction block after committing any changes
+ # specified in that block.
+ # See {Committing or Aborting}[rdoc-ref:PStore@Committing+or+Aborting].
#
- # Ends the current PStore#transaction, committing any changes to the data
- # store immediately.
- #
- # == Example:
- #
- # require "pstore"
- #
- # store = PStore.new("data_file.pstore")
- # store.transaction do # begin transaction
- # # load some data into the store...
- # store[:one] = 1
- # store[:two] = 2
- #
- # store.commit # end transaction here, committing changes
- #
- # store[:three] = 3 # this change is never reached
- # end
- #
- # *WARNING*: This method is only valid in a PStore#transaction. It will
- # raise PStore::Error if called at any other time.
- #
+ # Raises an exception if called outside a transaction block.
def commit
in_transaction
@abort = false
throw :pstore_abort_transaction
end
+
+ # Exits the current transaction block, ignoring any changes
+ # specified in that block.
+ # See {Committing or Aborting}[rdoc-ref:PStore@Committing+or+Aborting].
#
- # Ends the current PStore#transaction, discarding any changes to the data
- # store.
- #
- # == Example:
- #
- # require "pstore"
- #
- # store = PStore.new("data_file.pstore")
- # store.transaction do # begin transaction
- # store[:one] = 1 # this change is not applied, see below...
- # store[:two] = 2 # this change is not applied, see below...
- #
- # store.abort # end transaction here, discard all changes
- #
- # store[:three] = 3 # this change is never reached
- # end
- #
- # *WARNING*: This method is only valid in a PStore#transaction. It will
- # raise PStore::Error if called at any other time.
- #
+ # Raises an exception if called outside a transaction block.
def abort
in_transaction
@abort = true
throw :pstore_abort_transaction
end
+ # Defines a transaction block for the store.
+ # See {Transactions}[rdoc-ref:PStore@Transactions].
#
- # Opens a new transaction for the data store. Code executed inside a block
- # passed to this method may read and write data to and from the data store
- # file.
- #
- # At the end of the block, changes are committed to the data store
- # automatically. You may exit the transaction early with a call to either
- # PStore#commit or PStore#abort. See those methods for details about how
- # changes are handled. Raising an uncaught Exception in the block is
- # equivalent to calling PStore#abort.
- #
- # If _read_only_ is set to +true+, you will only be allowed to read from the
- # data store during the transaction and any attempts to change the data will
- # raise a PStore::Error.
+ # With argument +read_only+ as +false+, the block may contain any Ruby code,
+ # including calls to \PStore methods other #transaction.
#
- # Note that PStore does not support nested transactions.
+ # With argument +read_only+ as +true+, the block may not include calls
+ # to #transaction, #[]=, or #delete.
#
+ # Raises an exception if called within a transaction block.
def transaction(read_only = false) # :yields: pstore
value = nil
if !@thread_safe