key_value_name 0.0.1

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