From 8715ecd04b8bb4976b89913be4e790e5d15c4b74 Mon Sep 17 00:00:00 2001 From: BurdetteLamar Date: Mon, 27 Jun 2022 13:16:58 -0500 Subject: [PATCH] [ruby/pstore] Enhanced RDoc https://github.com/ruby/pstore/commit/81a266d88c --- lib/pstore.rb | 501 ++++++++++++++++++++++++++++++++++---------------- 1 file changed, 345 insertions(+), 156 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 +# temp = store.dup, 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 ) -# @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 initialize(page_name, author, contents) +# @page_name = page_name +# @revisions = Array.new +# add_revision(author, contents) +# end +# +# 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 +# end # -# ### Some time later... ### -# -# # 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 pstore.ultra_safe = true. -# 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] # - # Retrieves a value from the PStore file data, by _name_. The hierarchy of - # Ruby objects stored under that root _name_ will be returned. + # 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: # - # *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[:foo] # => 0 + # store[:nope] # => nil + # end # + # 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) # - # 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. + # Like #[], except that it accepts a default value for the store. + # If the root for the given +key+ does not exist: # - # *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 +default+ is +PStore::Error+. + # - Returns the value of +default+ otherwise: # + # 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. + # Creates or replaces an object or hierarchy of objects + # at the root for +key+: # - # == Example: + # store = PStore.new('t.store') + # store.transaction do + # store[:bat] = 3 + # end # - # require "pstore" - # - # 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 - # - # *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 an object hierarchy from the data store, by _name_. + # Removes and returns the root for +key+ if it exists: # - # *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. + # store = PStore.new('t.store') + # store.transaction do + # store[:bat] = 3 + # store.delete(:bat) + # end # + # 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. + # With argument +read_only+ as +false+, the block may contain any Ruby code, + # including calls to \PStore methods other #transaction. # - # 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. - # - # 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