key_value_name 0.0.1

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
+ SHA1:
3
+ metadata.gz: c1f8780bde799ea40e2fd042acd3893a16d506b1
4
+ data.tar.gz: 0c8a821f32cf6cbdd8681c559f27498011d64e25
5
+ SHA512:
6
+ metadata.gz: b13cf37a899c2be009c01cc3f94da5068bc2c495901f217d72f715444e8118f191cb96fcf9553527ac4f3f5879220c1a898e6a93d6c2545c8908462cf8dc8e34
7
+ data.tar.gz: eee937f19b07d827f3bd5bdf65b7c1b93935e8d4fe7f222b85c3f3e7afa0be570d008b1466db312ebff6bfab378e40c9e4712ca437e31e8ebd43318b470af650
@@ -0,0 +1,72 @@
1
+ # key_value_name
2
+
3
+ * https://github.com/jdleesmiller/key_value_name
4
+
5
+ ## Synopsis
6
+
7
+ Store key-value pairs in file names, for example parameter names and parameters for experiments or simulation runs.
8
+
9
+ This gem provides:
10
+
11
+ 1. Some standard naming conventions that work well across platforms by avoiding special characters in file names.
12
+
13
+ 2. Automatic formatting and type conversion for the parameters.
14
+
15
+ ## Usage
16
+
17
+ ```rb
18
+ require 'key_value_name'
19
+
20
+ ResultName = KeyValueName.new do |n|
21
+ n.key :seed, type: Numeric, format: '%d'
22
+ n.key :algorithm, type: Symbol
23
+ n.key :alpha, type: Numeric
24
+ n.extension :dat
25
+ end
26
+
27
+ name = ResultName.new(
28
+ seed: 123,
29
+ algorithm: :reticulating_splines,
30
+ alpha: 42.1)
31
+
32
+ name.to_s
33
+ # => seed-123.algorithm-reticulating_splines.alpha-42.1.dat
34
+
35
+ name.in('/tmp')
36
+ # => /tmp/seed-123.algorithm-reticulating_splines.alpha-42.1.dat
37
+
38
+ # Assuming a matching file exists in /tmp.
39
+ ResultName.glob('/tmp')
40
+ # => [#<struct ResultName seed=123, algorithm=:reticulating_splines, alpha=42.1>]
41
+ ```
42
+
43
+ ## INSTALLATION
44
+
45
+ ```
46
+ gem install key_value_name
47
+ ```
48
+
49
+ ## LICENSE
50
+
51
+ (The MIT License)
52
+
53
+ Copyright (c) 2017 John Lees-Miller
54
+
55
+ Permission is hereby granted, free of charge, to any person obtaining
56
+ a copy of this software and associated documentation files (the
57
+ 'Software'), to deal in the Software without restriction, including
58
+ without limitation the rights to use, copy, modify, merge, publish,
59
+ distribute, sublicense, and/or sell copies of the Software, and to
60
+ permit persons to whom the Software is furnished to do so, subject to
61
+ the following conditions:
62
+
63
+ The above copyright notice and this permission notice shall be
64
+ included in all copies or substantial portions of the Software.
65
+
66
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
67
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
68
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
69
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
70
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
71
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
72
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # A terrible idea, but also a useful one.
5
+ #
6
+ module KeyValueName
7
+ KEY_RX = /\w+/
8
+ KEY_VALUE_SEPARATOR = '-'
9
+ PAIR_SEPARATOR = '.'
10
+ PAIR_SEPARATOR_RX = /[.]/
11
+
12
+ def self.check_symbol(name)
13
+ raise ArgumentError, "bad symbol: #{name}" unless name =~ /\A#{KEY_RX}\z/
14
+ end
15
+
16
+ def self.new
17
+ builder = Builder.new
18
+ yield builder
19
+ builder.build
20
+ end
21
+ end
22
+
23
+ require_relative 'key_value_name/version'
24
+ require_relative 'key_value_name/marshalers'
25
+ require_relative 'key_value_name/spec'
26
+ require_relative 'key_value_name/builder'
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeyValueName
4
+ #
5
+ # Yield-based Domain-Specific Language (DSL) to build a `KeyValueName`.
6
+ #
7
+ class Builder
8
+ #
9
+ # Class methods of the returned `KeyValueName` class.
10
+ #
11
+ module ClassMethods
12
+ def read_hash(name)
13
+ key_value_name_spec.read(name)
14
+ end
15
+
16
+ def read(name)
17
+ new(read_hash(name))
18
+ end
19
+
20
+ def glob(path)
21
+ Dir.glob(File.join(path, '*')).map do |name|
22
+ basename = File.basename(name)
23
+ new(read_hash(basename)) if key_value_name_spec.matches?(basename)
24
+ end.compact
25
+ end
26
+ end
27
+
28
+ #
29
+ # Instance methods of the returned `KeyValueName` class.
30
+ #
31
+ module InstanceMethods
32
+ def in(folder)
33
+ File.join(folder, to_s)
34
+ end
35
+
36
+ def to_s
37
+ self.class.key_value_name_spec.write(self)
38
+ end
39
+ end
40
+
41
+ def initialize
42
+ @marshalers = {}
43
+ @extension = nil
44
+ end
45
+
46
+ def include_keys(key_value_name_klass)
47
+ spec = key_value_name_klass.key_value_name_spec
48
+ spec.marshalers.each do |name, marshaler|
49
+ check_no_existing_marshaler(name)
50
+ @marshalers[name] = marshaler
51
+ end
52
+ end
53
+
54
+ def key(name, type:, **kwargs)
55
+ KeyValueName.check_symbol(name)
56
+ raise ArgumentError, "bad type: #{type}" unless MARSHALERS.key?(type)
57
+ check_no_existing_marshaler(name)
58
+ @marshalers[name] = MARSHALERS[type].new(**kwargs)
59
+ end
60
+
61
+ def extension(ext) # rubocop:disable Style/TrivialAccessors
62
+ @extension = ext
63
+ end
64
+
65
+ def build # rubocop:disable Metrics/MethodLength
66
+ raise 'no keys defined' if @marshalers.none?
67
+
68
+ klass = Struct.new(*@marshalers.keys) do
69
+ def initialize(**kwargs)
70
+ super(*kwargs.keys)
71
+ kwargs.each { |k, v| self[k] = v }
72
+ end
73
+
74
+ include InstanceMethods
75
+
76
+ class <<self
77
+ include ClassMethods
78
+ attr_accessor :key_value_name_spec
79
+ end
80
+ end
81
+ klass.key_value_name_spec = Spec.new(@marshalers, @extension)
82
+ klass
83
+ end
84
+
85
+ private
86
+
87
+ def check_no_existing_marshaler(key)
88
+ raise ArgumentError, "already have key: #{key}" if @marshalers.key?(key)
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'marshalers/base'
3
+ require_relative 'marshalers/numeric_marshaler'
4
+ require_relative 'marshalers/symbol_marshaler'
5
+
6
+ module KeyValueName
7
+ MARSHALERS = {
8
+ Numeric => NumericMarshaler,
9
+ Symbol => SymbolMarshaler
10
+ }.freeze
11
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeyValueName
4
+ #
5
+ # A Marshaler handles conversion of typed values to and from strings.
6
+ #
7
+ class MarshalerBase
8
+ def initialize(**kwargs)
9
+ end
10
+
11
+ def matcher
12
+ raise NotImplementedError
13
+ end
14
+
15
+ def read(_string)
16
+ raise NotImplementedError
17
+ end
18
+
19
+ def write(_value)
20
+ raise NotImplementedError
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'scanf'
4
+
5
+ module KeyValueName
6
+ #
7
+ # Read and write numeric types with a general format string for
8
+ # `Kernel#format` (or `Kernel#sprintf`) and `String#scanf`.
9
+ #
10
+ # This seems like a nice idea, but ruby's `scanf` doesn't seem to handle
11
+ # all possible format strings. For example, you can format with `%.1f`, but
12
+ # scanning with that string does not work. To work around this problem,
13
+ # you can specify a separate `scan_format` that `scanf` can handle; for this
14
+ # example, it would be just `%f`.
15
+ #
16
+ class NumericMarshaler < MarshalerBase
17
+ def initialize(format: '%g', scan_format: nil)
18
+ @format_string = format
19
+ @scan_format_string = scan_format
20
+ end
21
+
22
+ attr_reader :format_string
23
+
24
+ def scan_format_string
25
+ @scan_format_string || format_string
26
+ end
27
+
28
+ def matcher
29
+ # This is the usual regex, except that it also has to match `%x` formats,
30
+ # so it allows hexadecimal whole numbers.
31
+ /[-+]?[0-9]*\.?[0-9a-f]+(?:e[-+]?[0-9]+)?/i
32
+ end
33
+
34
+ def read(string)
35
+ values = string.scanf(scan_format_string)
36
+ raise "failed to scan: #{string}" if values.empty?
37
+ values.first
38
+ end
39
+
40
+ def write(value)
41
+ format(format_string, value)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeyValueName
4
+ #
5
+ # Read and write symbol values.
6
+ #
7
+ class SymbolMarshaler < MarshalerBase
8
+ def matcher
9
+ KEY_RX
10
+ end
11
+
12
+ def read(string)
13
+ string.to_sym
14
+ end
15
+
16
+ def write(value)
17
+ KeyValueName.check_symbol(value)
18
+ value.to_s
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeyValueName
4
+ #
5
+ # Specify the keys and value types for a KeyValueName.
6
+ #
7
+ class Spec
8
+ def initialize(marshalers, extension)
9
+ @marshalers = marshalers
10
+ @extension = extension
11
+ @matcher = build_matcher
12
+
13
+ marshalers.freeze
14
+ freeze
15
+ end
16
+
17
+ attr_reader :marshalers
18
+ attr_reader :extension
19
+ attr_reader :matcher
20
+
21
+ def matches?(string)
22
+ string =~ matcher
23
+ end
24
+
25
+ def read(string)
26
+ raise ArgumentError, "bad filename: #{string}" unless string =~ matcher
27
+ Hash[marshalers.map.with_index do |(key, marshaler), index|
28
+ [key, marshaler.read(Regexp.last_match(index + 1))]
29
+ end]
30
+ end
31
+
32
+ def write(name)
33
+ string = name.each_pair.map do |key, value|
34
+ value_string = marshalers[key].write(value)
35
+ "#{key}#{KEY_VALUE_SEPARATOR}#{value_string}"
36
+ end.join(PAIR_SEPARATOR)
37
+ string += ".#{extension}" unless extension.nil?
38
+ string
39
+ end
40
+
41
+ private
42
+
43
+ def build_matcher
44
+ pair_rxs = marshalers.map do |name, marshaler|
45
+ /#{name}#{KEY_VALUE_SEPARATOR}(#{marshaler.matcher})/
46
+ end
47
+ pairs_matcher = pair_rxs.map(&:to_s).join(PAIR_SEPARATOR_RX.to_s)
48
+ extension_matcher = extension.nil? ? '' : /[.]#{extension}/.to_s
49
+ Regexp.new('\A' + pairs_matcher + extension_matcher + '\z')
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeyValueName
4
+ VERSION_MAJOR = 0
5
+ VERSION_MINOR = 0
6
+ VERSION_PATCH = 1
7
+ VERSION = [VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH].join('.')
8
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'minitest/autorun'
4
+ require 'fileutils'
5
+ require 'tmpdir'
6
+
7
+ require 'key_value_name'
8
+
9
+ class TestKeyValueName < MiniTest::Test
10
+ TestInteger = KeyValueName.new do |n|
11
+ n.key :a, type: Numeric, format: '%d'
12
+ end
13
+
14
+ TestHexNumeric = KeyValueName.new do |n|
15
+ n.key :b, type: Numeric, format: '%x'
16
+ end
17
+
18
+ TestFormattedNumeric = KeyValueName.new do |n|
19
+ n.key :c, type: Numeric, format: '%.3f', scan_format: '%f'
20
+ end
21
+
22
+ TestPaddedNumeric = KeyValueName.new do |n|
23
+ n.key :d, type: Numeric, format: '%04d'
24
+ end
25
+
26
+ TestSymbol = KeyValueName.new do |n|
27
+ n.key :a, type: Symbol
28
+ end
29
+
30
+ TestFloat = KeyValueName.new do |n|
31
+ n.key :a, type: Numeric
32
+ end
33
+
34
+ TestTwoIntegers = KeyValueName.new do |n|
35
+ n.include_keys TestInteger
36
+ n.include_keys TestHexNumeric
37
+ end
38
+
39
+ TestMixed = KeyValueName.new do |n|
40
+ n.key :id, type: Symbol
41
+ n.key :ordinal, type: Numeric, format: '%d'
42
+ n.key :value, type: Numeric
43
+ end
44
+
45
+ TestExtension = KeyValueName.new do |n|
46
+ n.key :big_number, type: Numeric, format: '%04d'
47
+ n.extension 'bin'
48
+ end
49
+
50
+ TestEKey = KeyValueName.new do |n|
51
+ n.key :x, type: Numeric
52
+ n.key :e, type: Numeric
53
+ end
54
+
55
+ def roundtrip(klass, string, args)
56
+ name = klass.read(string)
57
+ assert_equal args.keys, name.to_h.keys
58
+ args.each do |key, value|
59
+ assert_equal value, name[key]
60
+ end
61
+ assert_equal string, name.to_s
62
+ name
63
+ end
64
+
65
+ def test_integer_roundtrip
66
+ roundtrip(TestInteger, 'a-123', a: 123)
67
+ end
68
+
69
+ def test_hex_numeric_roundtrip
70
+ roundtrip(TestHexNumeric, 'b-ff', b: 255)
71
+ end
72
+
73
+ def test_formatted_numeric_roundtrip
74
+ roundtrip(TestFormattedNumeric, 'c-0.100', c: 0.1)
75
+ roundtrip(TestFormattedNumeric, 'c--0.200', c: -0.2)
76
+ end
77
+
78
+ def test_formatted_numeric_parse
79
+ assert_equal(-0.0013, TestFormattedNumeric.read('c--1.3e-3').c)
80
+ end
81
+
82
+ def test_padded_numeric_roundtrip
83
+ roundtrip(TestPaddedNumeric, 'd-0012', d: 12)
84
+ end
85
+
86
+ def test_symbol_roundtrip
87
+ roundtrip(TestSymbol, 'a-foo', a: :foo)
88
+ end
89
+
90
+ def test_symbol_accepts_strings
91
+ assert_equal 'a-foo', TestSymbol.new(a: 'foo').to_s
92
+ end
93
+
94
+ def test_float_roundtrip
95
+ roundtrip(TestFloat, 'a-2.25', a: 2.25)
96
+ end
97
+
98
+ def test_two_integers_roundtrip
99
+ roundtrip(TestTwoIntegers, 'a-123.b-ff', a: 123, b: 255)
100
+ end
101
+
102
+ def test_mixed_roundtrip
103
+ name = roundtrip(
104
+ TestMixed,
105
+ 'id-foo.ordinal-1234.value-2.5e-11',
106
+ id: :foo, ordinal: 1234, value: 2.5e-11
107
+ )
108
+ assert_equal :foo, name.id
109
+ assert_equal 1234, name.ordinal
110
+ assert_equal 2.5e-11, name.value
111
+ end
112
+
113
+ def test_extension_roundtrip
114
+ roundtrip(TestExtension, 'big_number-0123.bin', big_number: 123)
115
+ end
116
+
117
+ def test_e_key_roundtrip
118
+ # A file name called `x-1.e-2` could mean `x=1e-2` or `x=1`, `e=2`. It's
119
+ # still possible to distinguish between them if we know that we're looking
120
+ # for two keys.
121
+ roundtrip(TestEKey, 'x-1.e-2', x: 1, e: 2)
122
+ end
123
+
124
+ def test_missing_key
125
+ assert_raises do
126
+ TestInteger.new(b: 3)
127
+ end
128
+ assert_raises(ArgumentError) do
129
+ TestInteger.read('b-3')
130
+ end
131
+ end
132
+
133
+ def test_in_folder
134
+ assert_equal File.join('foo', 'a-1'), TestInteger.new(a: 1).in('foo')
135
+ end
136
+
137
+ def test_glob_with_integers
138
+ Dir.mktmpdir do |tmp|
139
+ FileUtils.touch TestInteger.new(a: 1).in(tmp)
140
+ FileUtils.touch TestInteger.new(a: 2).in(tmp)
141
+ names = TestInteger.glob(tmp).sort_by(&:a)
142
+ assert_equal 2, names.size
143
+ assert_equal 1, names[0].a
144
+ assert_equal 2, names[1].a
145
+ end
146
+ end
147
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: key_value_name
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - John Lees-Miller
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-02-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: gemma
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 4.1.0
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 4.1.0
27
+ description: Store key-value pairs in file names, e.g. parameter names and parameters.
28
+ email:
29
+ - jdleesmiller@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files:
33
+ - README.md
34
+ files:
35
+ - README.md
36
+ - lib/key_value_name.rb
37
+ - lib/key_value_name/builder.rb
38
+ - lib/key_value_name/marshalers.rb
39
+ - lib/key_value_name/marshalers/base.rb
40
+ - lib/key_value_name/marshalers/numeric_marshaler.rb
41
+ - lib/key_value_name/marshalers/symbol_marshaler.rb
42
+ - lib/key_value_name/spec.rb
43
+ - lib/key_value_name/version.rb
44
+ - test/key_value_name/key_value_name_test.rb
45
+ homepage: https://github.com/jdleesmiller/key_value_name
46
+ licenses: []
47
+ metadata: {}
48
+ post_install_message:
49
+ rdoc_options:
50
+ - "--main"
51
+ - README.md
52
+ - "--title"
53
+ - key_value_name-0.0.1 Documentation
54
+ require_paths:
55
+ - lib
56
+ required_ruby_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ requirements: []
67
+ rubyforge_project:
68
+ rubygems_version: 2.6.8
69
+ signing_key:
70
+ specification_version: 4
71
+ summary: Store key-value pairs in file names, e.g. parameter names and parameters.
72
+ test_files:
73
+ - test/key_value_name/key_value_name_test.rb