active_cabinet 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []