keyp 0.0.6 → 0.0.7

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bff8c5655d1b350b28208c20add4b0b3cf884fd2
4
- data.tar.gz: 53a122d43fda4ec7267bd77aea3626dc2e16dba6
3
+ metadata.gz: 2951607e2465cc3204c7f01d993802e75be59acb
4
+ data.tar.gz: efaa651f76f6d65eefde6316111bd62640790c43
5
5
  SHA512:
6
- metadata.gz: cf0bbf2bc59d7a578afd69641032677ab6ff61f7d78331603a63e30fab9bd3dfd68e88756e9c74d0566f189d438bf92ebe91c9ad43ee7bef74b9379d5dac793b
7
- data.tar.gz: baddf04bce4de7c751da7acf783a9527d00c31690cfa7d1a7697c402509c5cd912a9f2d1b4266d95d9f0eec08e79438b4628c2b1e3fd155559d04ef1797b2cc1
6
+ metadata.gz: d69defc81b8351eeeb6171738687dd2577c910e145ec67808ff56c72a6052c399d8be9bb399a38b857912d41d7de4d9574b50ea876206157ee383e8c122a8174
7
+ data.tar.gz: c4d11396115a4ebe2b0a1ed49f24cc2e1e72dbbada753aa0796d1fdaa301d5bfccd822f5d43168578b8dfca0f52249bf4d94e8d44b7e33442d7153f33c35e3f4
data/README.md CHANGED
@@ -12,12 +12,14 @@ Web applications generally need sensitive information, like salts and keys. Mana
12
12
 
13
13
  ## Important
14
14
 
15
- Keyp is still in early development and is experimental. As such it is very much a work in progress. Not all features
15
+ Keyp is still in early development. As such it is very much a work in progress. Not all features
16
16
  expressed in the documentation may be working. The documentation may be plain wrong. I don't recommend you use Keyp
17
17
  for any kind of production code at this point in this gem's lifecycle.
18
18
 
19
19
  ## Quick Tour
20
20
 
21
+ To install, see "Installation" below
22
+
21
23
  Keyp manages key value pairs in collections. As of version 0.0.4, Keyp refers to collections as *bags*. There is a
22
24
  default bag called *default*. Unless you specify a bag name, *default* will be used.
23
25
 
@@ -51,7 +53,8 @@ Keyp is not limited to just access keys. Any kind of string based name/value pai
51
53
 
52
54
  TODO: Improve description
53
55
 
54
- _CAVEAT:_ This gem is at a very early stage in development. Any and all functionality is subject to change.
56
+ _CAVEAT:_ This gem is at an early stage in development. Functionality is
57
+ subject to change depending on need and clarity.
55
58
 
56
59
  ## Installation
57
60
 
@@ -93,15 +96,14 @@ TODO: Write more detailed documentation, For now, see the quick start above and
93
96
  $ keyp --help
94
97
 
95
98
  ## Release Notes
99
+ * v 0.0.7 - Added loading a bag from the ENV. Added renaming bags Fixed error with keys of less than three characters.
100
+ * v 0.0.6 - Added copying key:values to the Ruby ENV in the library (still have to implement in the CLI)
96
101
  * v 0.0.5 - Fixed iso8601 error
97
102
  * v 0.0.4 - Fixed missing file error
98
103
 
99
104
  ## Development plan/Features to implement
100
105
 
101
- * Get basic functionality working soundly backed with effective tests
102
- * Implement .keyp directory setup
103
- * Add ENV vars to bag
104
- * Add bag vars to ENV
106
+ * Fill out rspec tests
105
107
  * Incorporate with shell configuration
106
108
  * Write very clear documentation
107
109
  * Implement import/Export to YAML and/or JSON
data/bin/keyp CHANGED
@@ -6,16 +6,18 @@
6
6
  # implementations. A good number and a few well known use Thor, so seems like a good idea to
7
7
  # figure out how to use Thor as well.
8
8
  #
9
- # I'm trying out some of the different impementations for Keyp, learning how they work, pros and cons
10
- # Then I'll pick a single approach after exploring them
9
+
11
10
 
12
11
  # TO run in the project dir:
13
12
  # ruby -Ilib ./bin/keyp
13
+ # or
14
+ # bundle exec bin/keyp
14
15
 
15
16
  # resolve bin path, ignoring symlinks
16
17
  require 'pathname'
17
18
  bin_file = Pathname.new(__FILE__).realpath
18
19
 
20
+ # The Keyp CLI uses GLI
19
21
  # http://davetron5000.github.io/gli/
20
22
  require 'gli'
21
23
 
@@ -24,13 +26,6 @@ require 'pp'
24
26
  require 'keyp'
25
27
  include GLI::App
26
28
 
27
- # keyp set key=value
28
- # keyp get key
29
- # keyp
30
-
31
-
32
- # keyp create bag
33
-
34
29
  program_desc 'A command line interface for the Keyp key:value manager'
35
30
 
36
31
  desc 'The Keyp bag to use'
@@ -42,6 +37,11 @@ switch [:d, :debug]
42
37
  desc 'Suppress hints: If invalid parameters are set, no suggestive feedback will be provided'
43
38
  switch [:s, :suppress]
44
39
 
40
+
41
+
42
+ version Keyp::VERSION
43
+
44
+
45
45
  # TODO: fix this because this implies that we either already have a bag or it will create a new one
46
46
  pre do |global_options, command, options, args|
47
47
  # initialize our store
@@ -57,8 +57,7 @@ pre do |global_options, command, options, args|
57
57
  puts "---- end prehook ---"
58
58
  end
59
59
 
60
- unless command.name == :setup
61
-
60
+ unless [:setup, :show, :delete].include? command.name
62
61
  # check if we are configured
63
62
  unless Keyp.configured?
64
63
  puts "Please run setup to use Keyp. Run with --help to view help menu"
@@ -125,7 +124,7 @@ command :set do |c|
125
124
  nvp = Keyp.parse_arg_string buff
126
125
 
127
126
  if nvp
128
- $bag[nvp[:key]] = nvp[:value]
127
+ $bag[nvp[:key]] = nvp[:value].strip
129
128
  elsif global_options[:suppress] == false
130
129
  # puts "Usage keyp set KEY1=VALUE1 [KEY2=VALUE2 ...]"
131
130
  puts "Usage: keyp set KEY=VALUE"
@@ -190,8 +189,11 @@ command :setup do |c|
190
189
  end
191
190
  end
192
191
 
193
- command :create do |c|
192
+ create_desc = 'Creates a new bag'
194
193
 
194
+ desc create_desc
195
+ command :create do |c|
196
+ c.desc create_desc
195
197
  c.action do |global_options, options, args|
196
198
  puts "keyp create"
197
199
  # TODO: add checking in case store already exists
@@ -206,14 +208,56 @@ command :create do |c|
206
208
  end
207
209
  end
208
210
 
211
+ delete_desc = 'Deletes an existing bag or set of bags matching the delete parameter'
212
+ desc delete_desc
209
213
  command :delete do |c|
214
+ c.desc 'forces delete (no prompting)'
215
+ c.switch [:f, :force]
216
+ c.desc "outputs the name of each file as it's deleted"
217
+ c.switch [:v, :verbose]
218
+ c.desc "Confirm deletion of each file. Currently not enabled"
219
+ c.switch [:c, :confirm]
220
+ c.desc delete_desc
210
221
  c.action do |global_options, options, args|
211
- puts "delete bag is not supported at this time"
212
- # TODO: prompt for verification
222
+ # TODO: add * for all bags
223
+ # TODO: enable prompting per file. Confirm skips the global confirm
224
+
225
+ help_now!('filename pattern is required') if args.empty?
226
+
227
+ if options[:force]
228
+ do_delete = true
229
+ else
230
+ puts "This will delete all the bags matching \"#{args[0]}\"."
231
+ print "Are you sure? (yes to confirm. Anything else to cancel) "
232
+ do_delete = STDIN.gets.chomp.upcase == 'YES'
233
+ end
234
+
235
+ if do_delete
236
+ # experimenting with different techniques
237
+ delete_type = 1
238
+ case delete_type
239
+ when 1
240
+ files = Dir["#{File.join(Keyp.home,args[0]+Keyp.ext)}"]
241
+ puts "going to delete files #{files}" if $debug
242
+ files.each do |f|
243
+ File.delete(f)
244
+ puts "deleted #{File.basename(f, Keyp.ext)}" if options[:verbose]
245
+ end
246
+ when 2
247
+ result = File.delete(args[0])
248
+ puts "delete results: #{result}"
249
+ #TODO: provide feedback
250
+ end
251
+ else
252
+ puts "delete bag(s) canceled."
253
+ end
213
254
  end
214
255
  end
215
256
 
257
+ show_desc = 'Lists all the bags in Keyp'
258
+ desc show_desc
216
259
  command :show do |c|
260
+ c.desc show_desc
217
261
  c.action do |global_options, options, args|
218
262
  # TODO: implement filtering, stats (like number of items in a bag, last updated, etc)
219
263
  # and just filename
@@ -224,13 +268,40 @@ command :show do |c|
224
268
  end
225
269
  end
226
270
 
227
- post do |global_options, command, options, args|
228
- # if bag.data is dirty, then save
229
- if $debug
230
- puts "post-hook, state of bag.dirty = #{$bag.dirty}"
271
+ rename_desc = 'Rename the given bag'
272
+ desc rename_desc
273
+ command :rename do |c|
274
+ #c.desc 'from bag name)'
275
+ #c.flag [:f, :from]
276
+ #c.desc 'to bag name'
277
+ #c.flag [:t, :to]
278
+ c.desc rename_desc
279
+ c.action do |global_options, options, args|
280
+ # get from_name and to_name
281
+ if args.count < 2
282
+ puts "TODO: rename help"
283
+ else
284
+ begin
285
+ res = Keyp::rename_bag(from: args[0], to: args[1])
286
+ puts "Renamed bag #{args[0]} to #{args[1]}"
287
+ rescue
288
+ puts "Unable to rename bag #{args[0]} to #{args[1]}"
289
+ # TODO: add reason
290
+ end
291
+
292
+ end
231
293
  end
294
+ end
295
+
232
296
 
233
- unless command.name == :setup
297
+ post do |global_options, command, options, args|
298
+
299
+ unless [:setup, :show, :delete].include? command.name
300
+ if $debug
301
+ puts "post-hook, state of bag.dirty = #{$bag.dirty}"
302
+ end
303
+ # if bag.data is dirty, then save
304
+ # TODO: check bag.save. if it checks dirty, then we don't need to check here
234
305
  $bag.save
235
306
  else
236
307
  true
@@ -43,6 +43,8 @@ module Keyp
43
43
  # Default home directory that keyp uses to store bags
44
44
  DEFAULT_KEYP_DIR = '.keyp'
45
45
 
46
+ # number of digits for the fractional seconds in decimal
47
+ TIMESTAMP_FS_DIGITS = 6
46
48
  #TODO: set this
47
49
  #ENV_VAR_NAME_REGEX =
48
50
 
@@ -64,7 +66,16 @@ module Keyp
64
66
  # it is recommended you restrict permissions
65
67
  #
66
68
  def self.home
67
- ENV['KEPY_HOME'] || File.join(ENV['HOME'], DEFAULT_KEYP_DIR)
69
+ #puts "Keyp::home, KEYP_HOME=#{ENV['KEYP_HOME']}"
70
+ ENV['KEYP_HOME'] || File.join(ENV['HOME'], DEFAULT_KEYP_DIR)
71
+ =begin
72
+ puts "home_dir=#{home_dir}"
73
+ if ENV['KEYP_HOME']
74
+ ENV['KEYP_HOME']
75
+ else
76
+ File.join(ENV['HOME'], DEFAULT_KEYP_DIR)
77
+ end
78
+ =end
68
79
  end
69
80
 
70
81
  ##
@@ -94,15 +105,13 @@ module Keyp
94
105
  # check if keyp directory exists
95
106
 
96
107
  unless Dir.exist?(home)
97
- puts "making directory #{home}"
98
108
  Dir.mkdir(home, 0700)
99
109
  if Dir.exist?(home)
100
110
  home
101
111
  else
102
- nil
112
+ raise "unable to create Keyp directory #{home}"
103
113
  end
104
114
  else
105
- Puts "#{home} already exists"
106
115
  nil
107
116
  end
108
117
  end
@@ -159,25 +168,14 @@ module Keyp
159
168
  ##
160
169
  # Creates a new bag if one does not already exist with the given name
161
170
  #
171
+ # raises exception if bag already exists
172
+ # NOTE: We might want to change this
162
173
  def self.create_bag(name, options = {} )
163
- time_now = Time.now.utc.iso8601
164
- file_data = {}
165
- file_data['meta'] = {
166
- 'name' => name,
167
- 'description' => '',
168
- 'created_at' => time_now,
169
- 'updated_at' => time_now
170
- }
171
- file_data['data'] = nil
172
174
  unless exist? name
173
- File.open(bag_path(name), 'w') do |f|
174
- f.write file_data.to_yaml
175
- f.chmod(0600)
176
- end
175
+ Bag.new(name)
177
176
  else
178
- raise "Unable to create a new store at #{filepath}. One already exists."
177
+ raise "Unable to create a new bag \"#{name}\". It already exists."
179
178
  end
180
- bag name
181
179
  end
182
180
 
183
181
  ##
@@ -193,6 +191,33 @@ module Keyp
193
191
  end
194
192
  end
195
193
 
194
+ ##
195
+ #
196
+ # == options
197
+ # +:from+ current name of the bag (rename from)
198
+ # +:to+ new name of the bag (rename to)
199
+ #
200
+ def self.rename_bag(options = {})
201
+
202
+ #TODO: Implement this
203
+ # validate_options(:from,:to)
204
+
205
+ from_name = options[:from]
206
+ to_name = options[:to]
207
+
208
+ unless from_name || to_name
209
+ raise "required parameters are missing. Requires both :from and :to"
210
+ end
211
+
212
+ # if current bag does not exist, we raise and exception
213
+ unless exist?(from_name)
214
+ raise ("cannot rename #{from_name} because it does not exist")
215
+ end
216
+ bag = bag(from_name)
217
+ bag.rename(to_name)
218
+ #bag(from_name).rename(to_name)
219
+ end
220
+
196
221
  def self.parse_arg_string(arg_string, options={})
197
222
 
198
223
  mode = options[:token_mode] || :default
@@ -4,21 +4,27 @@ module Keyp
4
4
  # Some inspiration:
5
5
  # http://stackoverflow.com/questions/2680523/dry-ruby-initialization-with-hash-argument
6
6
  #
7
- # TODO: add handling so that keys (hierarchical keys too) are accessed as members instead of
8
- # hash access
7
+ # TODO: add handling so that keys (hierarchical keys too) are accessed as members instead of hash access
9
8
  #
10
- # TODO: move to own file, rename to "Bag"
11
9
  # TODO: i18n error messages
10
+ # TODO: handle symbol to string and back for keys. Need instance setting to handle validating keys as symbols
12
11
  #
13
12
  class Bag
14
13
 
15
- attr_reader :keypdir, :dirty
16
- attr_accessor :name, :data, :file_hash
14
+ # TODO: Make name attr_reader
15
+ # TODO: See if we need data as an attr_accessor If not then it makes
16
+ # it easier to encapsulate handling of string/symbol keys
17
+ attr_reader :keypdir, :dirty, :meta
18
+ attr_accessor :data, :file_hash
19
+
20
+ def name
21
+ @meta['name']
22
+ end
17
23
 
18
24
  ##
19
25
  # Returns the full path of this Bag's file
20
26
  def keypfile
21
- File.join(@keypdir, @name+@ext)
27
+ File.join(@keypdir, name+@ext)
22
28
  end
23
29
 
24
30
  ##
@@ -28,39 +34,44 @@ module Keyp
28
34
  # +keypdir+
29
35
  # +read_only+
30
36
  # +ext+
31
- def initialize(name, options = {})
37
+ def initialize(bag_name, options = {})
32
38
  # I'm not happy with how creating instance variables works. There must be a cleaner way
33
- @name = name
39
+ @meta = { 'name' => bag_name}
34
40
  options.each do |k,v|
35
- puts "processing options #{k} = #{v}"
41
+ #JB set debug log: puts "processing options #{k} = #{v}"
36
42
  instance_variable_set("@#{k}", v) unless v.nil?
37
43
  end
38
- # set attributes not set by params
39
44
 
45
+ # set attributes not set by params
40
46
  @keypdir ||= Keyp::home
41
47
  @read_only ||= false
42
48
  @ext ||= Keyp::DEFAULT_EXT
43
- #@keypfile = config_path
44
- # load our resource
45
-
46
- # load config file into hashes
47
- # not the most efficient thing, but simpler and safe enough for now
48
49
 
49
50
  unless File.exist? keypfile
50
- puts "Bag.initialize, create_bag #{keypfile}"
51
- Keyp::create_bag(name)
51
+ #JB set debug log: puts "Bag.initialize, create_bag #{keypfile}"
52
+ #TODO: move this to its own method
53
+ time_now = Time.now.utc.iso8601(TIMESTAMP_FS_DIGITS)
54
+ file_data = {}
55
+ file_data['meta'] = {
56
+ 'name' => bag_name,
57
+ 'description' => '',
58
+ 'created_at' => time_now,
59
+ 'updated_at' => time_now
60
+ }
61
+ file_data['data'] = nil
62
+ File.open(Keyp::bag_path(bag_name), 'w') do |f|
63
+ f.write file_data.to_yaml
64
+ f.chmod(0600)
65
+ end
52
66
  end
53
- file_data = load(keypfile)
54
-
55
- @meta = file_data[:meta]
56
- @data = file_data[:data]|| {}
57
- @file_hash = file_data[:file_hash]
67
+ load_file
58
68
  @dirty = false
59
69
  end
60
70
 
61
71
  ##
62
72
  #
63
73
  def [](key)
74
+ # TODO: convert from any symbols to string
64
75
  @data[key]
65
76
  end
66
77
 
@@ -74,6 +85,7 @@ module Keyp
74
85
  # Sets a key:value pair
75
86
  # NOTE: This may be made protected
76
87
  def set_prop(key, value)
88
+ # TODO
77
89
  unless @read_only
78
90
  # TODO: check if data has been modified
79
91
  # maybe there is a way hash tells us its been modified. If not then
@@ -83,22 +95,19 @@ module Keyp
83
95
  @dirty = true
84
96
  end
85
97
  else
86
- raise "Bag #{@name} is read only"
98
+ raise "Bag #{name} is read only"
87
99
  end
88
100
  end
89
101
 
90
102
  ##
91
103
  # Deletes the key:value pair from this bag for the given key
92
104
  def delete(key)
93
- unless @read_only
94
- if @data.key? key
95
- @dirty = true
96
- end
97
- val = @data.delete(key)
98
- else
99
- raise "Bag #{@name} is read only"
105
+ if @read_only
106
+ raise "Cannot delete key \"#{key}\". Bag \"#{name}\" is read only."
100
107
  end
101
- val
108
+
109
+ @dirty = true if @data.key? key
110
+ @data.delete(key)
102
111
  end
103
112
 
104
113
  ##
@@ -108,6 +117,14 @@ module Keyp
108
117
  end
109
118
 
110
119
 
120
+ def read_only?
121
+ @read_only
122
+ end
123
+
124
+ def key?(key)
125
+ @data.key?(key)
126
+ end
127
+
111
128
  ##
112
129
  # Adds key/value pairs from this bag to the Ruby ENV
113
130
  # NOTE: Currently in development.
@@ -136,7 +153,7 @@ module Keyp
136
153
  overwrite = options[:overwrite] || false
137
154
  pattern = options[:sysvar] if options.key?(:sysvar)
138
155
 
139
- pattern ||= /(...)/
156
+ pattern ||= /(..*)/
140
157
  @data.each do |key,value|
141
158
  if pattern.match(key)
142
159
  # TODO: add overwrite checking
@@ -147,9 +164,71 @@ module Keyp
147
164
  assigned
148
165
  end
149
166
 
167
+ ##
168
+ # load environment variables into this bag
169
+ # === Options
170
+ # +:pattern+ Applies keys matching this pattern
171
+ # +:overwrite+ true to overwrite existing keys, false (default) to not overwrite existing keys
172
+ #
173
+ #
174
+ def load_from_env(options = {})
175
+ assigned = {}
176
+ overwrite = options[:overwrite] || false
177
+ pattern = options[:pattern] || /(..*)/
178
+ ENV.each do | key, value|
179
+ if pattern.match(key)
180
+ unless overwrite && @data.key?(key)
181
+ @data[key] = value
182
+ assigned[key] = value
183
+ end
184
+ else
185
+ puts "load_from_env. No match for #{key}"
186
+ end
187
+ end
188
+ assigned
189
+ end
150
190
 
151
191
  # TODO add from hash
152
192
 
193
+ ##
194
+ # renames the bag to +new_name+
195
+ # returns new bag name if succesful, raises exception if not
196
+ #
197
+ def rename(new_name, options = {})
198
+
199
+ # Since we are not changing any key pairs, the only meta to be changed is +name+
200
+ #
201
+ # TODO: Check for valid key name
202
+ #TODO: change to try to create a new file and lock it. If we can, then we fill it
203
+ if Keyp::exist?(new_name)
204
+ raise "Bag rename error. Target \"#{new_name}\" already exists."
205
+ end
206
+
207
+ if @dirty
208
+ # We don't want to allow renaming a bag with modified values because it will complicate rollback
209
+ #TODO: improve error message
210
+ raise "Can't rename a modified bag. Save first"
211
+ end
212
+
213
+ # TODO: treat the following as a transaction so if the bag file can't be renamed then
214
+ # the old file is restored unmodified
215
+ #curr_name = @meta['name']
216
+ curr_name = name
217
+ old_file = Keyp::bag_path(curr_name)
218
+ to_file = Keyp::bag_path(new_name)
219
+ #TODO: Add file locking for concurrency protection
220
+ @meta['name'] = new_name
221
+ @meta['last_name'] =curr_name
222
+ @renaming = true
223
+ # saves with the new name
224
+ save
225
+ @renaming = nil
226
+ # now remove the old file
227
+ File.delete(old_file)
228
+
229
+ load_file
230
+ @meta['name']
231
+ end
153
232
 
154
233
  def import(filename)
155
234
  raise "import not yet supported"
@@ -165,39 +244,12 @@ module Keyp
165
244
  # TODO: def to_s
166
245
 
167
246
  ##
168
- # Give full path, attempt to load
169
- # sticking with YAML format for now
170
- # May add new file format later in which case we'll
171
- # TODO: consider changing to class method
172
- def load (config_path)
173
- #config_data = {}
174
- # Get extension
175
- file_ext = File.extname(config_path)
176
-
177
- # check
178
- # only YAML supported for initial version. Will consider adapters to
179
- # abstract the persistence layer
180
-
181
-
182
- # TODO: make this hardcoded case a hash of helpers
183
- # TODO: Add two sections: Meta and data, then return as hash
184
-
185
- if file_ext.downcase == Keyp::DEFAULT_EXT
186
-
187
- # Either we are arbitrarily creating directories when
188
- # given a path for a file that doesn't exist
189
- # or we have special behavior for the default dir
190
- # or we just fault and let the caller deal with it
191
- unless File.exist? config_path
192
- raise "Keyp config file not found: #{config_path}"
193
- end
194
-
195
- file_data = YAML.load_file(config_path)
196
-
197
- else
198
- raise "Keyp version x only supports YAML for config files. You tried a #{file_ext}"
199
- end
200
- { meta: file_data['meta'], data: file_data['data']||{}, file_hash: file_data.hash }
247
+ #
248
+ def load_file
249
+ file_data = Bag::load_file(keypfile)
250
+ @meta = file_data[:meta]
251
+ @data = file_data[:data]|| {}
252
+ @file_hash = file_data[:file_hash]
201
253
  end
202
254
 
203
255
  ##
@@ -209,22 +261,25 @@ module Keyp
209
261
  if @read_only
210
262
  raise "This bag instance is read only"
211
263
  end
212
- if @dirty
264
+ if @dirty || @renaming
213
265
  # lock file
214
266
  # read checksum
215
267
  # if checksum matches our saved checksum then update file and release lock
216
268
  # otherwise, raise
217
269
  # TODO: implement merge from updated file and raise only if conflict
218
270
  begin
219
- file_data = { 'meta' => @meta, 'data' => @data }
220
-
221
271
  if File.exist? keypfile
222
- read_file_data = load(keypfile)
272
+ read_file_data = Bag::load_file(keypfile)
223
273
  unless @file_hash == read_file_data[:file_hash]
224
274
  raise "Will not write to #{keypfile}\nHashes differ. Expected hash =#{@file_hash}\n" +
225
275
  "found hash #{read_file_data[:file_hash]}"
226
276
  end
227
277
  end
278
+ if @dirty && !@renaming
279
+ @meta['updated_at'] = Time.now.utc.iso8601(Keyp::TIMESTAMP_FS_DIGITS)
280
+ end
281
+
282
+ file_data = { 'meta' => @meta, 'data' => @data }
228
283
  File.open(keypfile, 'w') do |f|
229
284
  f.write file_data.to_yaml
230
285
  end
@@ -233,7 +288,49 @@ module Keyp
233
288
  # TODO: log error
234
289
 
235
290
  end
291
+ else
292
+ puts "save called but not saving (not dirty or renaming flag set"
293
+ end
294
+ end
295
+
296
+
297
+ # Class methods
298
+ ##
299
+ # Give full path, attempt to load
300
+ # sticking with YAML format for now
301
+ # May add new file format later in which case we'll
302
+ # TODO: Make this a class method and an instance method
303
+ # that calls the class method and does the instance assignment
304
+ def self.load_file (config_path)
305
+ # Get extension
306
+ file_ext = File.extname(config_path)
307
+
308
+ # check
309
+ # only YAML supported for initial version. Will consider adapters to
310
+ # abstract the persistence layer
311
+
312
+
313
+ # TODO: make this hardcoded case a hash of helpers
314
+ # TODO: Add two sections: Meta and data, then return as hash
315
+
316
+ if file_ext.downcase == Keyp::DEFAULT_EXT
317
+
318
+ # Either we are arbitrarily creating directories when
319
+ # given a path for a file that doesn't exist
320
+ # or we have special behavior for the default dir
321
+ # or we just fault and let the caller deal with it
322
+ unless File.exist? config_path
323
+ raise "Keyp config file not found: #{config_path}"
324
+ end
325
+
326
+ file_data = YAML.load_file(config_path)
327
+
328
+ else
329
+ raise "Keyp version x only supports YAML for config files. You tried a #{file_ext}"
236
330
  end
331
+ { meta: file_data['meta'], data: file_data['data']||{}, file_hash: file_data.hash }
237
332
  end
238
- end
239
- end
333
+
334
+
335
+ end # class Bag
336
+ end # module Keyp
@@ -1,3 +1,3 @@
1
1
  module Keyp
2
- VERSION = "0.0.6"
2
+ VERSION = "0.0.7"
3
3
  end
@@ -2,44 +2,53 @@ require 'spec_helper'
2
2
 
3
3
  describe Keyp::Bag do
4
4
 
5
+ def bag_name_for_testing
6
+ # TODO: pseudo random bag name generation to a helper
7
+ "grue_eats_you_when_it_is_dark_#{Time.now.strftime("%Y%m%d%H%M%S%L")}"
8
+ end
9
+
5
10
  context "is empty" do
6
11
  before (:each) do
7
- puts "before : is empty"
8
- @bag = Keyp::Bag.new 'testing123'
12
+ @bag_name = bag_name_for_testing
13
+ @bag = Keyp::Bag.new @bag_name
9
14
  end
10
15
 
11
16
  it "should return an empty hash" do
12
-
13
17
  @bag.data.size.should == 0
14
18
  end
15
- #it "should have metadata" do
16
- # @bag.meta['created_on'].should_not == nil
17
- #end
19
+ after (:each) do
20
+ Keyp.delete_bag @bag_name if Keyp.exist? @bag_name
21
+ end
18
22
  end
19
23
 
20
24
  context "is not empty" do
21
25
  before (:each) do
22
- puts "before : is not empty"
23
- @bag = Keyp::Bag.new 'grue_eats_you'
26
+ @bag_name = bag_name_for_testing
27
+ @bag = Keyp::Bag.new @bag_name
24
28
  @bag['LIGHTS'] = 'out'
25
29
  end
26
30
 
27
31
  it "should return a non-empty hash" do
28
32
  @bag.data.size.should > 0
29
33
  end
34
+ after (:each) do
35
+ Keyp.delete_bag @bag_name if Keyp.exist? @bag_name
36
+ end
30
37
  end
31
38
 
32
39
  context "environment variables" do
33
40
  before (:each) do
34
- # TODO: pseudo random bag name generation to a helper
35
- bag_name = "grue_eats_you_when_it_is_dark_#{Time.now.strftime("%Y%m%d%H%M%S%L")}"
36
- @bag = Keyp::Bag.new bag_name
41
+ @bag_name = bag_name_for_testing
42
+ @bag = Keyp::Bag.new @bag_name
37
43
  end
38
44
  it "should copy all vars" do
39
45
  testvars = {
40
46
  'ALPHA' => 'First in the phonetic alphabet',
41
47
  'BRAVO' => 'Second in the phonetic alphabet',
42
- 'CHARLIE' => 'Third in the phonetic alphabet'
48
+ 'CHARLIE' => 'Third in the phonetic alphabet',
49
+ '_' => 'a single underscore',
50
+ 'a' => 'first letter in the alphabet',
51
+ 'bb' => 'two of the second letter in the alphabet'
43
52
  }
44
53
 
45
54
  testvars.each { |key, value| @bag[key] = value }
@@ -50,14 +59,39 @@ describe Keyp::Bag do
50
59
  ENV[key].should == value
51
60
  end
52
61
  end
62
+
63
+ it 'should receive all vars' do
64
+ test_keyvals = {
65
+ 'foo' => 'bar',
66
+ 'biz' => 'baz',
67
+ 'GRUE' => 'Eats you in the dark',
68
+ 'a' => 'first letter in the alphabet',
69
+ 'bb' => 'two of the second letter in the alphabet'
70
+ }
71
+
72
+ test_keyvals.each do |key,value|
73
+ ENV[key] = value
74
+ end
75
+ @bag.load_from_env
76
+ ENV.each do |key,value|
77
+ #puts "testing key=#{key}"
78
+ @bag.key?(key).should == true
79
+ @bag[key].should == value
80
+ end
81
+ end
82
+
83
+ it 'should not overwrite existing keys'
84
+ it 'should match filtering pattern'
85
+ after (:each) do
86
+ Keyp.delete_bag @bag_name if Keyp.exist? @bag_name
87
+ end
53
88
  end
54
89
 
55
- context "bag state" do
90
+ context 'Bag state' do
56
91
 
57
92
  before (:each) do
58
- # TODO: pseudo random bag name generation to a helper
59
- bag_name = "grue_eats_you_when_it_is_dark_#{Time.now.strftime("%Y%m%d%H%M%S%L")}"
60
- @bag = Keyp::Bag.new bag_name
93
+ @bag_name = bag_name_for_testing
94
+ @bag = Keyp::Bag.new @bag_name
61
95
  end
62
96
 
63
97
  it 'should not set the dirty flag when no items are in the bag' do
@@ -92,10 +126,102 @@ describe Keyp::Bag do
92
126
  @bag['KEY1'] = 'value1'
93
127
  @bag.dirty.should == false
94
128
  end
129
+
130
+ it 'should allow assigning a key if not read only'
131
+ it 'should not allow assigning a key if read only'
132
+ after (:each) do
133
+ Keyp.delete_bag @bag_name if Keyp.exist? @bag_name
134
+ end
135
+ end
136
+
137
+ context 'Metadata' do
138
+
139
+ before (:each) do
140
+ @bag_name = bag_name_for_testing
141
+ @bag = Keyp::Bag.new @bag_name
142
+ end
143
+
144
+ it "should have metadata" do
145
+ # @bag.meta['created_on'].should_not == nil
146
+ end
147
+
148
+ it 'should not update created_at after bag is initially created' do
149
+ created_at = @bag.meta['created_at']
150
+ @bag['foo'] = 'bar'
151
+ @bag.save
152
+ @bag.meta['created_at'].should == created_at
153
+ end
154
+
155
+ it 'should update updated_at when bag is saved' do
156
+ updated_at = @bag.meta['updated_at']
157
+ @bag['foo'] = 'bar'
158
+ @bag.save
159
+ @bag.meta['updated_at'].should > updated_at
160
+ end
161
+
162
+ it 'should not update updated_at if bag has not been saved' do
163
+ updated_at = @bag.meta['updated_at']
164
+ @bag['foo'] = 'bar'
165
+ @bag.meta['updated_at'].should == updated_at
166
+ end
167
+
168
+ it 'should use metadata for the bag name' do
169
+ def @bag.set_name(name)
170
+ @meta['name'] = name
171
+ end
172
+ test_name = 'norwegian_blue'
173
+ @bag.set_name(test_name)
174
+ @bag.name.should == test_name
175
+ end
176
+ after (:each) do
177
+ Keyp.delete_bag @bag_name if Keyp.exist? @bag_name
178
+ end
95
179
  end
96
180
 
181
+ context 'Bag management' do
182
+ before (:each) do
183
+ @from_name = bag_name_for_testing
184
+ @from_bag = Keyp::Bag.new @from_name
185
+ sleep(1)
186
+ @to_name = bag_name_for_testing
187
+ end
188
+
189
+ it 'should rename if new name does not exist' do
190
+ bag = @from_bag
191
+ kp = {
192
+ 'key1' => 'value1',
193
+ 'key2' => 'value2'
194
+ }
195
+
196
+ # seed a couple of kv pairs just to keep it a bit real
197
+ # but we should also test with an empty bag
198
+ kp.each {|k,v| bag[k] = v }
199
+ bag.save
200
+ before_meta = {}
201
+ bag.meta.each { |k,v| before_meta[k] = v }
202
+ bag.name.should == @from_name
203
+ @to_name.should_not == @from_name
204
+ # we know the to_name does not exist
205
+ result = bag.rename(@to_name)
206
+ bag.name.should == @to_name
207
+ bag.meta['name'].should == @to_name
208
+ bag.meta['created_at'].should == before_meta['created_at']
209
+ # Since we are not changing any key pairs, updated_at doesn't change
210
+ bag.meta['updated_at'].should == before_meta['updated_at']
211
+ bag.meta['name'].should_not == before_meta['name']
212
+ end
213
+
214
+ it 'should not rename if new name exists' do
215
+
216
+ end
217
+ after (:each) do
218
+ Keyp.delete_bag @from_name if Keyp.exist? @from_name
219
+ Keyp.delete_bag @to_name if Keyp.exist? @to_name
220
+ end
221
+ end
222
+
223
+
97
224
  it 'should return a key with data member'
98
225
  it 'should return a key acting as a hash'
99
- it 'should allow assigning a key if not read only'
100
- it 'should not allow assigning a key if read only'
226
+
101
227
  end
@@ -1,18 +1,25 @@
1
1
  require 'spec_helper'
2
+ require 'tmpdir'
3
+ require 'fileutils'
2
4
 
3
5
  describe Keyp do
4
6
 
5
- context "CONSTANTS" do
7
+ def bag_name_for_testing
8
+ # TODO: pseudo random bag name generation to a helper
9
+ "bring_a_towel_#{Time.now.strftime("%Y%m%d%H%M%S%L")}"
10
+ end
11
+
12
+ context 'CONSTANTS' do
6
13
  it 'should return correct version string' do
7
14
  #Keyp.version_string.should == "Keyp version #{Keyp::VERSION}"
8
- Keyp::VERSION.should == '0.0.6'
15
+ Keyp::VERSION.should == '0.0.7'
9
16
  end
10
17
 
11
18
  it 'should specify default store' do
12
19
  Keyp::DEFAULT_BAG.should == 'default'
13
20
  end
14
21
 
15
- it "should specifify default store extension" do
22
+ it 'should specifify default store extension' do
16
23
  Keyp::DEFAULT_EXT.should == '.yml'
17
24
  end
18
25
 
@@ -21,16 +28,41 @@ describe Keyp do
21
28
  end
22
29
  end
23
30
 
24
- context "Keyp directory" do
31
+ context 'Default Keyp directory' do
25
32
  it 'should return correct default keyp dir' do
26
33
  # DEFAULT_KEYP_DIRNAME = ENV['KEYP_DIRNAME'] || '.keyp'
27
34
  Keyp::home.should == File.join(ENV['HOME'], '.keyp')
28
35
  end
36
+ end
37
+
38
+ context 'Alternate Keyp directory' do
39
+
40
+
41
+ before (:each) do
42
+ @root_tempdir = Dir.mktmpdir
43
+ @temp_keyp_dir = File.join(@root_tempdir,Keyp::DEFAULT_KEYP_DIR)
44
+ ENV['KEYP_HOME'] = @temp_keyp_dir
45
+ end
46
+
47
+ it 'should be able to override default keyp dir' do
48
+ puts "ENV['KEYP_HOME']=#{ENV['KEYP_HOME']}"
49
+ keyp_dir = Keyp::setup
50
+ keyp_dir.should == Keyp::home
51
+ keyp_dir.should == @temp_keyp_dir
52
+ Keyp::home.should == ENV['KEYP_HOME']
53
+
54
+ end
29
55
 
30
- it 'should be able to override default keyp dir'
56
+ #TODO: Keyp::setup should return nil when trying to setup to an existing dir
57
+ # Check if we can describe a method
58
+ after (:each) do
59
+ puts "AFTER called, going to try to delete #{@tempdir}"
60
+ FileUtils.rm_rf @root_tempdir
61
+ ENV['KEYP_HOME'] = nil
62
+ end
31
63
  end
32
64
 
33
- context "Bag management" do
65
+ context 'Bag management' do
34
66
  it 'should return default bag' do
35
67
  keyper = Keyp::bag
36
68
  keyper.name.should == 'default'
@@ -42,15 +74,65 @@ describe Keyp do
42
74
  Keyp.exist?(keyper.name).should == true
43
75
  end
44
76
 
45
- it "should say a bag does not exist" do
77
+ it 'should say a bag does not exist' do
46
78
  # TODO: add bag name generation to a helper
47
79
  bag_name = "grue_eats_you_when_it_is_dark_#{Time.now.strftime("%Y%m%d%H%M%S%L")}"
48
80
  Keyp.exist?(bag_name).should_not == true
49
81
  end
50
-
51
82
  end
52
83
 
84
+ context 'Renaming bags' do
85
+ # NOTE: Yeah, this code and the bag_spec code are redundant
86
+ before (:each) do
87
+ @from_name = bag_name_for_testing
88
+ if Keyp::exist?(@from_name)
89
+ puts "Rename bag, bag #{@from_name} already exists"
90
+ else
91
+ puts "Rename bag, bag #{@from_name} does NOT exist"
92
+ end
93
+ @from_bag = Keyp.create_bag(@from_name)
94
+ sleep(1)
95
+ @to_name = bag_name_for_testing
96
+ end
53
97
 
98
+ it 'should rename bag if it exists and new name does not' do
99
+ bag = @from_bag
100
+ kp = {
101
+ 'key1' => 'value1',
102
+ 'key2' => 'value2'
103
+ }
104
+
105
+ # seed a couple of kv pairs just to keep it a bit real
106
+ # but we should also test with an empty bag
107
+ kp.each {|k,v| bag[k] = v }
108
+ bag.save
109
+ before_meta = {}
110
+ bag.meta.each { |k,v| before_meta[k] = v }
111
+ bag.name.should == @from_name
112
+ @to_name.should_not == @from_name
113
+ result = Keyp.rename_bag(from: @from_name, to: @to_name)
114
+ Keyp.exist?(@from_name).should == false
115
+ Keyp.exist?(@to_name).should == true
116
+ bag = Keyp.bag @to_name
117
+ bag.name.should == @to_name
118
+ bag.meta['name'].should == @to_name
119
+ bag.meta['created_at'].should == before_meta['created_at']
120
+ # Since we are not changing any key pairs, updated_at doesn't change
121
+ bag.meta['updated_at'].should == before_meta['updated_at']
122
+ kp.each do |k,v|
123
+ bag.key?(k).should == true
124
+ bag[k].should == v
125
+ end
126
+ end
127
+
128
+ it 'should not rename bag if current name does not exist'
129
+ it 'should not rename bag if new name exists'
130
+
131
+ after (:each) do
132
+ Keyp.delete_bag @from_name if Keyp.exist? @from_name
133
+ Keyp.delete_bag @to_name if Keyp.exist? @to_name
134
+ end
135
+ end
54
136
 
55
137
 
56
138
  # Braindump of tests
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keyp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Baldwin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-04-03 00:00:00.000000000 Z
11
+ date: 2014-05-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: gli