active_cabinet 0.1.0

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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 774c19d270fc5767611ad0b42eaa06f7b98c3bae2bdfa2e4f8f0e382629daf59
4
+ data.tar.gz: 4d48778ba0cd2294b047d0292c87e963c4c16823a1effe6f93a207afeb182713
5
+ SHA512:
6
+ metadata.gz: 9e9e84e8ae733967cee88489b45020c68609ef67172d5a0c8b9e3540d946d8a0a9d32e139362f95a3d0aa3f545c1a612c1993f3a4327f67559506d98c3ab8620
7
+ data.tar.gz: 3a9a02b9f0c0df27479db9ba4f6ae8c6e17cd3f10caad24fe00e669c7402f19b3dddc4461553d25e9b7cf61588b4bf668cf9b194bbf3cce27a319de7c2bb297f
@@ -0,0 +1,136 @@
1
+ ActiveCabinet
2
+ ==================================================
3
+
4
+ ---
5
+
6
+ ActiveCabinet is an ActiveRecord-inspired interface for HashCabinet, the
7
+ file-basd key-object store.
8
+
9
+ It allows you to create models that are stored in a file-based key-value
10
+ store, backed by Ruby's built in [SDBM].
11
+
12
+ ---
13
+
14
+ Installation
15
+ --------------------------------------------------
16
+
17
+ $ gem install active_cabinet
18
+
19
+
20
+
21
+ Usage
22
+ --------------------------------------------------
23
+
24
+ Before trying these examples, create a directory named `db` - this is the
25
+ default directory where the cabinet files are stored.
26
+
27
+ ```ruby
28
+ require 'active_cabinet'
29
+
30
+ # Define a model
31
+ class Song < ActiveCabinet
32
+ end
33
+
34
+ # Create the model, and store it in the cabinet
35
+ # Each object must have at least an `id` and can have any number of
36
+ # attributes
37
+ song = Song.create id: 1, title: 'Moonchild', artist: 'Iron Maiden'
38
+ p song
39
+ #=> #<Song @attributes={:id=>1, :title=>"Moonchild", :artist=>"Iron Maiden"}>
40
+
41
+ # Get all records
42
+ Song.all #=> Array of Song objects
43
+ Song.count #=> 1
44
+
45
+ # Retrieve a specific record
46
+ Song[1]
47
+ Song.find 1
48
+
49
+ # Read one attribute or all attributes from a record
50
+ song.album
51
+ song.attributes
52
+
53
+ # Update a single attribute
54
+ song.year = 1988
55
+ song.save
56
+
57
+ # Update multiple attributes
58
+ song.update year: 1988, artist: 'Metallica'
59
+ song.update! year: 1988, artist: 'Metallica' # this variant also saves
60
+ ```
61
+
62
+ ### Restricting / allowing certain attributes
63
+
64
+ You may specify required arguments. Records without these attributes will
65
+ not be saved. Note that `id` is always required
66
+
67
+ ```ruby
68
+ class Song < ActiveCabinet
69
+ required_attributes :title, :artist
70
+ end
71
+
72
+ song = Song.new title: "Moonchild"
73
+ song.valid? # => false
74
+ song.error # => "missing required attributes: [:artist, :id]"
75
+
76
+ song = Song.new id: 1, title: "Moonchild", artist: 'Iron Maiden'
77
+ song.valid? # => true
78
+
79
+ # Additional attributes are still allowed
80
+ song.year = 1988
81
+ song.valid? # => true
82
+ ```
83
+
84
+ You can also restrict the allowed optional attributes
85
+
86
+ ```
87
+ class Song < ActiveCabinet
88
+ required_attributes :title
89
+ optional_attributes :artist
90
+ end
91
+
92
+ song = Song.new id: 1, title: 'Moonchild', album: 'Seventh Son of a Seventh Son'
93
+ song.valid? # => false
94
+ song.error # => "invalid attributes: [:album]"
95
+ ```
96
+
97
+ In order to enforce only the required attributes, without optional ones, set
98
+ the value of `optional_attributes` to `false`
99
+
100
+ ```ruby
101
+ class Song < ActiveCabinet
102
+ required_attributes :title
103
+ optional_attributes false
104
+ end
105
+
106
+ song = Song.new id: 1, title: 'Moonchild', artist: 'Iron Maiden'
107
+ song.valid? # => false
108
+ song.error # => "invalid attributes: [:artist]"
109
+ ```
110
+
111
+ ### Configuring storage path
112
+
113
+ By default, `ActiveCabinet` stores all its files (two files per model) in the
114
+ `./db` directory. The file name is determined by the name of the class.
115
+
116
+ You can override both of these values
117
+
118
+ ```ruby
119
+ # Set the based directory for all cabinets
120
+ ActiveCabinet::Config.dir = "cabinets"
121
+
122
+ # Set the filename of your model
123
+ class Song < ActiveCabinet
124
+ cabinet_name "songs_collection"
125
+ end
126
+ ```
127
+
128
+ For the full documentation, see the [Documentation on RubyDoc][docs]
129
+
130
+
131
+ [SDBM]: https://ruby-doc.org/stdlib-2.6.3/libdoc/sdbm/rdoc/SDBM.html
132
+ [docs]: https://rubydoc.info/gems/active_cabinet
133
+
134
+ ---
135
+
136
+ [SDBM]: https://ruby-doc.org/stdlib-2.7.1/libdoc/sdbm/rdoc/SDBM.html
@@ -0,0 +1,167 @@
1
+ require 'hash_cabinet'
2
+ require 'active_cabinet/metaclass'
3
+
4
+ # @!attribute [r] attributes
5
+ # @return [Hash] the attributes of the record
6
+ # @!attribute [r] error
7
+ # @return [String, nil] the last validation error, after calling {valid?}
8
+ class ActiveCabinet
9
+ attr_reader :attributes, :error
10
+
11
+ # @!group Constructor
12
+
13
+ # Initializes a new record with {attributes}
14
+ #
15
+ # @param [Hash] attributes record attributes
16
+ def initialize(attributes = {})
17
+ @attributes = attributes.transform_keys(&:to_sym)
18
+ end
19
+
20
+ # @!group Attribute Management
21
+
22
+ # Returns an array containing {required_attributes} and {optional_attributes}.
23
+ #
24
+ # @return [Array<Symbol>] array of required attribute keys.
25
+ def allowed_attributes
26
+ self.class.allowed_attributes
27
+ end
28
+
29
+ # Returns an array of required record attributes.
30
+ #
31
+ # @see ActiveCabinet.required_attributes.
32
+ # @return [Array<Symbol>] the array of required attributes
33
+ def required_attributes
34
+ self.class.required_attributes
35
+ end
36
+
37
+ # Returns an array of optional record attributes.
38
+ #
39
+ # @see ActiveCabinet.optional_attributes.
40
+ # @return [Array<Symbol>] the array of optional attributes
41
+ def optional_attributes
42
+ self.class.optional_attributes
43
+ end
44
+
45
+ # Returns +true+ if the object is valid.
46
+ #
47
+ # @return [Boolean] +true+ if the record is valid.
48
+ def valid?
49
+ missing_keys = required_attributes - attributes.keys
50
+ if missing_keys.any?
51
+ @error = "missing required attributes: #{missing_keys}"
52
+ return false
53
+ end
54
+
55
+ if !optional_attributes or optional_attributes.any?
56
+ invalid_keys = attributes.keys - allowed_attributes
57
+ if invalid_keys.any?
58
+ @error = "invalid attributes: #{invalid_keys}"
59
+ return false
60
+ end
61
+ end
62
+
63
+ true
64
+ end
65
+
66
+ # @!group Dynamic Attribute Accessors
67
+
68
+ # Provides read/write access to {attributes}
69
+ def method_missing(method_name, *args, &blk)
70
+ name = method_name
71
+ return attributes[name] if attributes.has_key? name
72
+
73
+ suffix = nil
74
+
75
+ if name.to_s.end_with?('=', '?')
76
+ suffix = name[-1]
77
+ name = name[0..-2].to_sym
78
+ end
79
+
80
+ case suffix
81
+ when "="
82
+ attributes[name] = args.first
83
+
84
+ when "?"
85
+ !!attributes[name]
86
+
87
+ else
88
+ super
89
+
90
+ end
91
+ end
92
+
93
+ # Returns +true+ when calling +#respond_to?+ with an attribute name.
94
+ #
95
+ # @return [Boolean] +true+ if there is a matching attribute.
96
+ def respond_to_missing?(method_name, include_private = false)
97
+ name = method_name
98
+ name = name[0..-2].to_sym if name.to_s.end_with?('=', '?')
99
+ attributes.has_key?(name) || super
100
+ end
101
+
102
+ # @!group Loading and Saving
103
+
104
+ # Reads the attributes of the record from the cabinet and returns the
105
+ # record itself. If the record is not stored on disk, returns +nil+.
106
+ #
107
+ # @return [self, nil] the object or +nil+ if the object is not stored.
108
+ def reload
109
+ return nil unless saved?
110
+ update cabinet[id]
111
+ self
112
+ end
113
+
114
+ # Saves the record to the cabinet if it is valid. Returns the record on
115
+ # success, or +false+ on failure.
116
+ #
117
+ # @return [self, false] the record or +false+ on failure.
118
+ def save
119
+ if valid?
120
+ cabinet[id] = attributes
121
+ self
122
+ else
123
+ false
124
+ end
125
+ end
126
+
127
+ # Returns +true+ if the record exists in the cabinet.
128
+ #
129
+ # @note This method only verifies that the ID of the record exists. The
130
+ # attributes of the instance and the stored record may differ.
131
+ #
132
+ # @return [Boolean] +true+ if the record is saved in the cabinet.
133
+ def saved?
134
+ cabinet.key? id
135
+ end
136
+
137
+ # Update the record with new or modified attributes.
138
+ #
139
+ # @param [Hash] new_attributes record attributes
140
+ def update(new_attributes)
141
+ @attributes = attributes.merge new_attributes
142
+ end
143
+
144
+ # Update the record with new or modified attributes, and save.
145
+ #
146
+ # @param [Hash] new_attributes record attributes
147
+ def update!(new_attributes)
148
+ update new_attributes
149
+ save
150
+ end
151
+
152
+ # @!group Utilities
153
+
154
+ # Returns a Hash of attributes
155
+ #
156
+ # @return [Hash<Symbol, Object>] the hash of attriibutes/
157
+ def to_h
158
+ attributes
159
+ end
160
+
161
+ protected
162
+
163
+ def cabinet
164
+ self.class.cabinet
165
+ end
166
+
167
+ end
@@ -0,0 +1,21 @@
1
+ class ActiveCabinet
2
+ # Configure the global behavior of {ActiveCabinet}.
3
+ #
4
+ # @example
5
+ # # Change the directory where cabinets are stored
6
+ # ActiveCabinet::Config.dir = "cabinets"
7
+ #
8
+ # @attr_writer [String] dir Sets the base directory for all cabinet files (default +'db'+).
9
+ class Config
10
+ class << self
11
+ attr_writer :dir
12
+
13
+ # Returns the base directory for all cabinet files.
14
+ #
15
+ # @return [String] the base directory for all cabinet files.
16
+ def dir
17
+ @dir ||= 'db'
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,170 @@
1
+ require 'hash_cabinet'
2
+ require 'forwardable'
3
+ require 'active_cabinet/config'
4
+
5
+ # {ActiveCabinet} lets you create a +HashCabinet+ collection model by
6
+ # subclassing {ActiveCabinet}:
7
+ #
8
+ # class Song < ActiveCabinet
9
+ # end
10
+ #
11
+ # Now, you can perform CRUD operations on this collection, which will be
12
+ # persisted to disk:
13
+ #
14
+ # # Create
15
+ # Song.create id: 1, title: 'Moonchild', artist: 'Iron Maiden'
16
+ #
17
+ # # Read
18
+ # moonchild = Song[1] # or Song.find 1
19
+ #
20
+ # # Update
21
+ # moonchild.title = "22 Acacia Avenue"
22
+ # moonchild.save
23
+ # # or
24
+ # moonchild.update! title: "22 Acacia Avenue"
25
+ #
26
+ # # Delete
27
+ # Song.delete 1
28
+ #
29
+ class ActiveCabinet
30
+ class << self
31
+ extend Forwardable
32
+ def_delegators :cabinet, :count, :delete, :empty?, :keys, :size
33
+
34
+ # @!group Creating Records
35
+
36
+ # Creates and saves a new record instance.
37
+ #
38
+ # @param [String] id the record id.
39
+ # @param [Hash] attributes the attributes to create.
40
+ def []=(id, attributes)
41
+ create attributes.merge(id: id)
42
+ end
43
+
44
+ # Creates and saves a new record instance.
45
+ #
46
+ # @param [Hash] attributes the attributes to create.
47
+ # @return [Object] the record.
48
+ def create(attributes)
49
+ record = new attributes
50
+ record.save || record
51
+ end
52
+
53
+ # @!group Reading Records
54
+
55
+ # Returns all records.
56
+ #
57
+ # @return [Array] array of all records.
58
+ def all
59
+ cabinet.values.map do |attributes|
60
+ new(attributes)
61
+ end
62
+ end
63
+
64
+ # Returns all records, as an Array of Hashes.
65
+ #
66
+ # @return [Array] array of all records.
67
+ def all_attributes
68
+ cabinet.values
69
+ end
70
+
71
+ # Returns an array of records for which the block returns true.
72
+ #
73
+ # @yieldparam [Object] record all record instances.
74
+ def where
75
+ all.select { |record| yield record }
76
+ end
77
+
78
+ # Returns the record matching the +id+.
79
+ #
80
+ # @return [Object, nil] the object if found, or +nil+.
81
+ def find(id)
82
+ attributes = cabinet[id]
83
+ attributes ? new(attributes) : nil
84
+ end
85
+ alias [] find
86
+
87
+ # @!group Deleting Records
88
+
89
+ # Deletes a record matching the +id+
90
+ #
91
+ # @param [String] id the record ID.
92
+ # @return [Boolean] +true+ on success, +false+ otherwise.
93
+ def delete(id)
94
+ !!cabinet.delete(id)
95
+ end
96
+
97
+ # Deletes all records.
98
+ def drop
99
+ cabinet.clear
100
+ end
101
+
102
+ # @!group Attribute Management
103
+
104
+ # Returns an array containing {required_attributes} and {optional_attributes}.
105
+ #
106
+ # @return [Array<Symbol>] array of required attribute keys.
107
+ def allowed_attributes
108
+ (optional_attributes || []) + required_attributes
109
+ end
110
+
111
+ # Sets the required record attribute names.
112
+ #
113
+ # @param [Array<Symbol>] *attributes one or more attribute names.
114
+ # @return [Array<Symbol>] the array of required attributes.
115
+ def required_attributes(*args)
116
+ args = args.first if args.first.is_a? Array
117
+ if args.any?
118
+ @required_attributes = args
119
+ @required_attributes.push :id unless @required_attributes.include? :id
120
+ @required_attributes
121
+ else
122
+ @required_attributes ||= [:id]
123
+ end
124
+ end
125
+
126
+ # Sets the optional record attribute names.
127
+ #
128
+ # @param [Array<Symbol>] *attributes one or more attribute names.
129
+ # @return [Array<Symbol>] the array of optional attributes.
130
+ def optional_attributes(*args)
131
+ args = args.first if args.first.is_a? Array
132
+ if args.first === false
133
+ @optional_attributes = false
134
+ elsif args.any?
135
+ @optional_attributes = *args
136
+ else
137
+ @optional_attributes.nil? ? [] : @optional_attributes
138
+ end
139
+ end
140
+
141
+ # @!group Utilities
142
+
143
+ # Returns all records as a hash, with record IDs as the keys.
144
+ def to_h
145
+ cabinet.to_h.map { |id, attributes| [id, new(attributes)] }.to_h
146
+ end
147
+
148
+ # Returns the +HashCabinet+ instance.
149
+ #
150
+ # @return [HashCabinet] the +HashCabinet+ object.
151
+ def cabinet
152
+ @cabinet ||= HashCabinet.new "#{Config.dir}/#{cabinet_name}"
153
+ end
154
+
155
+ # Returns or sets the cabinet name.
156
+ # Defaults to the name of the class, lowercase.
157
+ #
158
+ # @param [String] name the name of the cabinet file.
159
+ # @return [String] name the name of the cabinet file.
160
+ def cabinet_name(new_name = nil)
161
+ if new_name
162
+ @cabinet = nil
163
+ @cabinet_name = new_name
164
+ else
165
+ @cabinet_name ||= self.to_s.downcase.gsub('::', '_')
166
+ end
167
+ end
168
+
169
+ end
170
+ end
@@ -0,0 +1,3 @@
1
+ class ActiveCabinet
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_cabinet
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Danny Ben Shitrit
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-09-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: hash_cabinet
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.1'
27
+ description: ActiveRecord-inspired interface for HashCabinet, the file-basd key-object
28
+ store.
29
+ email: db@dannyben.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - README.md
35
+ - lib/active_cabinet.rb
36
+ - lib/active_cabinet/config.rb
37
+ - lib/active_cabinet/metaclass.rb
38
+ - lib/active_cabinet/version.rb
39
+ homepage: https://github.com/dannyben/active_cabinet
40
+ licenses:
41
+ - MIT
42
+ metadata: {}
43
+ post_install_message:
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: 2.5.0
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubygems_version: 3.1.2
59
+ signing_key:
60
+ specification_version: 4
61
+ summary: Database-like interface for HashCabinet
62
+ test_files: []