key_value_name 0.0.1 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: c1f8780bde799ea40e2fd042acd3893a16d506b1
4
- data.tar.gz: 0c8a821f32cf6cbdd8681c559f27498011d64e25
2
+ SHA256:
3
+ metadata.gz: 432b1073bbe7b163324f8059f7ea2f27e9f47b6c1ed30fccc8cc46cd87331feb
4
+ data.tar.gz: ca4d6bcb5682b54c1560a63b959efd7ba607e70f306d45b97d16b53634d200e3
5
5
  SHA512:
6
- metadata.gz: b13cf37a899c2be009c01cc3f94da5068bc2c495901f217d72f715444e8118f191cb96fcf9553527ac4f3f5879220c1a898e6a93d6c2545c8908462cf8dc8e34
7
- data.tar.gz: eee937f19b07d827f3bd5bdf65b7c1b93935e8d4fe7f222b85c3f3e7afa0be570d008b1466db312ebff6bfab378e40c9e4712ca437e31e8ebd43318b470af650
6
+ metadata.gz: 0f9e333b8925c5bdf275defef6744e7def94f3f56f0501e42a55a9e676ab4623b0f670adeb8d22416b212f3781bfebad091cfdfdd0758906c4df7bdfa92eb297
7
+ data.tar.gz: 23ca7d09b805628438c4f68db43cac7e5d1a12d297b1886769c642fe15d1628c6b067a30dc9d4f548fa8b230b9e3ef8c329fb8cbe700cec0adf8a30d637368ef
data/README.md CHANGED
@@ -2,9 +2,12 @@
2
2
 
3
3
  * https://github.com/jdleesmiller/key_value_name
4
4
 
5
+ [![Build Status](https://github.com/jdleesmiller/key_value_name/actions/workflows/ci.yml/badge.svg)](https://github.com/jdleesmiller/key_value_name/actions/workflows/ci.yml)
6
+ [![Gem Version](https://badge.fury.io/rb/key_value_name.png)](http://badge.fury.io/rb/key_value_name)
7
+
5
8
  ## Synopsis
6
9
 
7
- Store key-value pairs in file names, for example parameter names and parameters for experiments or simulation runs.
10
+ An 'object-file system mapper' for managing data files. Key-value pairs describing the data in each file are stored in its name. Useful for managing data files for experiments or simulation runs.
8
11
 
9
12
  This gem provides:
10
13
 
@@ -12,32 +15,45 @@ This gem provides:
12
15
 
13
16
  2. Automatic formatting and type conversion for the parameters.
14
17
 
18
+ 3. A schema builder to organize files and folders and declare what parameters are allowed.
19
+
15
20
  ## Usage
16
21
 
17
22
  ```rb
18
23
  require 'key_value_name'
19
24
 
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
+ Results = KeyValueName.schema do
26
+ folder :simulation do
27
+ key :seed, type: Integer, format: '%06d'
28
+ key :algorithm, type: Symbol
29
+ key :alpha, type: Float
30
+
31
+ file :stats, :csv
32
+ end
25
33
  end
26
34
 
27
- name = ResultName.new(
35
+ results = Results.new(root: 'data/results')
36
+
37
+ sim = results.simulation.new(
28
38
  seed: 123,
29
39
  algorithm: :reticulating_splines,
30
40
  alpha: 42.1)
31
41
 
32
- name.to_s
33
- # => seed-123.algorithm-reticulating_splines.alpha-42.1.dat
42
+ sim.to_s
43
+ # => "data/results/simulation-seed-000123.algorithm-reticulating_splines.alpha-42.1"
44
+
45
+ sim.stats_csv.to_s
46
+ # => "data/results/simulation-seed-000123.algorithm-reticulating_splines.alpha-42.1/stats.csv"
47
+
48
+ # Pretend we've run a simulation and written the results...
49
+ sim.stats_csv.touch!
34
50
 
35
- name.in('/tmp')
36
- # => /tmp/seed-123.algorithm-reticulating_splines.alpha-42.1.dat
51
+ # List the results
52
+ results.simulation.all
53
+ # => [#<struct Results::Simulation seed=123, algorithm=:reticulating_splines, alpha=42.1>]
37
54
 
38
- # Assuming a matching file exists in /tmp.
39
- ResultName.glob('/tmp')
40
- # => [#<struct ResultName seed=123, algorithm=:reticulating_splines, alpha=42.1>]
55
+ results.simulation.all.map(&:stats_csv).map(&:to_s)
56
+ # => ["data/results/simulation-seed-000123.algorithm-reticulating_splines.alpha-42.1/stats.csv"]
41
57
  ```
42
58
 
43
59
  ## INSTALLATION
@@ -50,7 +66,7 @@ gem install key_value_name
50
66
 
51
67
  (The MIT License)
52
68
 
53
- Copyright (c) 2017 John Lees-Miller
69
+ Copyright (c) 2017-2025 John Lees-Miller
54
70
 
55
71
  Permission is hereby granted, free of charge, to any person obtaining
56
72
  a copy of this software and associated documentation files (the
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeyValueName
4
+ #
5
+ # TODO
6
+ #
7
+ module ContainerBuilder
8
+ def file(...)
9
+ @builders << FileBuilder.new(...)
10
+ end
11
+
12
+ def folder(...)
13
+ @builders << FolderBuilder.new(...)
14
+ end
15
+
16
+ def extend_with_builders(klass)
17
+ @builders.each do |builder|
18
+ child_class = builder.build
19
+ klass.const_set(builder.class_name, child_class)
20
+ if builder.singular?
21
+ build_singular(builder, klass, child_class)
22
+ else
23
+ build_collection(builder, klass, child_class)
24
+ end
25
+ end
26
+ klass
27
+ end
28
+
29
+ private
30
+
31
+ def build_singular(builder, klass, child_class)
32
+ klass.class_eval do
33
+ define_method(builder.name) do
34
+ child = child_class.new
35
+ child.parent = self
36
+ child
37
+ end
38
+ end
39
+ end
40
+
41
+ def build_collection(builder, klass, child_class)
42
+ klass.class_eval do
43
+ define_method(builder.name) do
44
+ Collection.new(child_class, self)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeyValueName
4
+ #
5
+ # Build a file KeyValueName.
6
+ #
7
+ class FileBuilder < KeyValueBuilder
8
+ def initialize(name, *extension, class_name: nil, &block)
9
+ KeyValueName.check_symbol(name) if name
10
+ @name = name
11
+ @extension = extension
12
+ @class_name = class_name
13
+ super(&block)
14
+ end
15
+
16
+ def extension(extension)
17
+ raise 'extension already set' if @extension.any?
18
+ @extension = Array(extension)
19
+ end
20
+
21
+ def name
22
+ name_parts.join('_').to_sym
23
+ end
24
+
25
+ def class_name
26
+ @class_name || default_class_name
27
+ end
28
+
29
+ def build
30
+ klass = super
31
+ klass.class_eval do
32
+ include FileName::InstanceMethods
33
+
34
+ class <<self
35
+ include FileName::ClassMethods
36
+ end
37
+ end
38
+ klass.key_value_name_spec = make_spec
39
+ klass
40
+ end
41
+
42
+ private
43
+
44
+ def name_parts
45
+ [@name] + @extension
46
+ end
47
+
48
+ def default_class_name
49
+ name_parts.map { |part| KeyValueName.camelize(part) }.join('')
50
+ end
51
+
52
+ def make_spec
53
+ prefix = @name
54
+ prefix = "#{prefix}#{KEY_VALUE_SEPARATOR}" if prefix && @marshalers.any?
55
+ suffix = ".#{@extension.join('.')}" if @extension.any?
56
+ Spec.new(@marshalers, prefix, suffix)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeyValueName
4
+ #
5
+ # Build a folder KeyValueName.
6
+ #
7
+ class FolderBuilder < KeyValueBuilder
8
+ include ContainerBuilder
9
+
10
+ def initialize(name, class_name: nil, &block)
11
+ KeyValueName.check_symbol(name)
12
+ @name = name
13
+ @class_name = class_name
14
+ @builders = []
15
+ super(&block)
16
+ end
17
+
18
+ attr_reader :name
19
+
20
+ def class_name
21
+ @class_name || KeyValueName.camelize(name)
22
+ end
23
+
24
+ def build
25
+ klass = super
26
+ klass.class_eval do
27
+ include FolderName::InstanceMethods
28
+
29
+ class <<self
30
+ include FolderName::ClassMethods
31
+ end
32
+ end
33
+ klass.key_value_name_spec = make_spec
34
+ extend_with_builders(klass)
35
+ end
36
+
37
+ private
38
+
39
+ def make_spec
40
+ prefix = @name
41
+ prefix = "#{prefix}#{KEY_VALUE_SEPARATOR}" if prefix && @marshalers.any?
42
+ Spec.new(@marshalers, prefix)
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeyValueName
4
+ #
5
+ # Yield-based Domain-Specific Language (DSL) to build a `KeyValueName`.
6
+ #
7
+ class KeyValueBuilder
8
+ def initialize(&block)
9
+ @marshalers = {}
10
+ instance_eval(&block) if block_given?
11
+ end
12
+
13
+ def singular?
14
+ @marshalers.none?
15
+ end
16
+
17
+ def include_keys(key_value_name_klass)
18
+ spec = key_value_name_klass.key_value_name_spec
19
+ spec.marshalers.each do |name, marshaler|
20
+ check_no_existing_marshaler(name)
21
+ @marshalers[name] = marshaler
22
+ end
23
+ end
24
+
25
+ def key(name, type:, **kwargs)
26
+ KeyValueName.check_symbol(name)
27
+ KeyValueName.check_marshaler(type)
28
+ check_no_existing_marshaler(name)
29
+ @marshalers[name] = MARSHALERS[type].new(**kwargs)
30
+ end
31
+
32
+ def build
33
+ struct_args = @marshalers.any? ? @marshalers.keys : [nil]
34
+ Struct.new(*struct_args, keyword_init: true)
35
+ end
36
+
37
+ private
38
+
39
+ def check_no_existing_marshaler(key)
40
+ raise ArgumentError, "already have key: #{key}" if @marshalers.key?(key)
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeyValueName
4
+ #
5
+ # The root builder. Can contain files and folders only.
6
+ #
7
+ class SchemaBuilder
8
+ include ContainerBuilder
9
+
10
+ def initialize(&block)
11
+ @builders = []
12
+ instance_eval(&block)
13
+ end
14
+
15
+ def build
16
+ extend_with_builders(Class.new(Schema))
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeyValueName
4
+ #
5
+ # A collection of KeyValueNames.
6
+ #
7
+ class Collection
8
+ include Enumerable
9
+
10
+ def initialize(klass, parent)
11
+ @klass = klass
12
+ @parent = parent
13
+ end
14
+
15
+ def new(*args)
16
+ object = @klass.new(*args)
17
+ object.parent = @parent
18
+ object
19
+ end
20
+
21
+ def each(&block)
22
+ all.each(&block)
23
+ end
24
+
25
+ def all
26
+ @klass.glob(@parent).sort
27
+ end
28
+
29
+ def where(**kwargs)
30
+ all.select do |name|
31
+ kwargs.all? do |key, value|
32
+ name[key] == value
33
+ end
34
+ end
35
+ end
36
+
37
+ def find_by(**kwargs)
38
+ where(**kwargs).first
39
+ end
40
+ end
41
+ end
@@ -5,19 +5,22 @@ module KeyValueName
5
5
  # A Marshaler handles conversion of typed values to and from strings.
6
6
  #
7
7
  class MarshalerBase
8
- def initialize(**kwargs)
9
- end
8
+ def initialize(**kwargs) end
10
9
 
11
10
  def matcher
12
11
  raise NotImplementedError
13
12
  end
14
13
 
15
- def read(_string)
14
+ def parse(_string)
16
15
  raise NotImplementedError
17
16
  end
18
17
 
19
- def write(_value)
18
+ def generate(_value)
20
19
  raise NotImplementedError
21
20
  end
21
+
22
+ def to_comparable(value)
23
+ value
24
+ end
22
25
  end
23
26
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeyValueName
4
+ #
5
+ # Read and write a boolean flag.
6
+ #
7
+ class BooleanMarshaler < MarshalerBase
8
+ def matcher
9
+ /true|false/i
10
+ end
11
+
12
+ def parse(string)
13
+ string == 'true'
14
+ end
15
+
16
+ def generate(value)
17
+ value ? 'true' : 'false'
18
+ end
19
+
20
+ def to_comparable(value)
21
+ value ? 1 : 0
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeyValueName
4
+ #
5
+ # Read and write float types.
6
+ #
7
+ class FloatMarshaler < MarshalerBase
8
+ def initialize(format: '%g')
9
+ @format_string = format
10
+ end
11
+
12
+ attr_reader :format_string
13
+
14
+ def matcher
15
+ /[-+]?[0-9]*\.?[0-9]+(?:e[-+]?[0-9]+)?/i
16
+ end
17
+
18
+ def parse(string)
19
+ string.to_f
20
+ end
21
+
22
+ def generate(value)
23
+ format(format_string, value.to_s)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KeyValueName
4
+ #
5
+ # Read and write integer types. Positive binary, octal and hexadecimal numbers
6
+ # without prefixes are also supported. Padding with zeros is OK, but padding
7
+ # with spaces will not work.
8
+ #
9
+ class IntegerMarshaler < MarshalerBase
10
+ def initialize(format: '%d')
11
+ @format_string = format
12
+ end
13
+
14
+ attr_reader :format_string
15
+
16
+ def matcher
17
+ /[-+]?[0-9a-f]+/i
18
+ end
19
+
20
+ def base
21
+ case format_string
22
+ when /b\z/i then 2
23
+ when /o\z/i then 8
24
+ when /x\z/i then 16
25
+ else 10
26
+ end
27
+ end
28
+
29
+ def parse(string)
30
+ string.to_i(base)
31
+ end
32
+
33
+ def generate(value)
34
+ format(format_string, value)
35
+ end
36
+ end
37
+ end
@@ -9,11 +9,11 @@ module KeyValueName
9
9
  KEY_RX
10
10
  end
11
11
 
12
- def read(string)
12
+ def parse(string)
13
13
  string.to_sym
14
14
  end
15
15
 
16
- def write(value)
16
+ def generate(value)
17
17
  KeyValueName.check_symbol(value)
18
18
  value.to_s
19
19
  end
@@ -1,11 +1,23 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require_relative 'marshalers/base'
3
- require_relative 'marshalers/numeric_marshaler'
4
+ require_relative 'marshalers/boolean_marshaler'
5
+ require_relative 'marshalers/float_marshaler'
6
+ require_relative 'marshalers/integer_marshaler'
4
7
  require_relative 'marshalers/symbol_marshaler'
5
8
 
9
+ #
10
+ #
11
+ #
6
12
  module KeyValueName
7
13
  MARSHALERS = {
8
- Numeric => NumericMarshaler,
14
+ boolean: BooleanMarshaler,
15
+ Float => FloatMarshaler,
16
+ Integer => IntegerMarshaler,
9
17
  Symbol => SymbolMarshaler
10
18
  }.freeze
19
+
20
+ def self.check_marshaler(type)
21
+ raise ArgumentError, "bad type: #{type}" unless MARSHALERS.key?(type)
22
+ end
11
23
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module KeyValueName
6
+ module FileName
7
+ #
8
+ # Instance method mixin for a file.
9
+ #
10
+ module InstanceMethods
11
+ include Name::InstanceMethods
12
+
13
+ def exist?
14
+ File.exist?(to_s)
15
+ end
16
+
17
+ #
18
+ # Ensure that the parent folder exists.
19
+ #
20
+ # @return [self]
21
+ #
22
+ def mkdir!
23
+ parent.mkdir!
24
+ self
25
+ end
26
+
27
+ def touch!
28
+ retried ||= false
29
+ FileUtils.touch(to_s)
30
+ self
31
+ rescue Errno::ENOENT
32
+ FileUtils.mkdir_p(File.dirname(to_s))
33
+ raise if retried
34
+ retried = true
35
+ retry
36
+ end
37
+
38
+ def destroy!
39
+ FileUtils.rm_f(to_s)
40
+ end
41
+ end
42
+
43
+ #
44
+ # Class methods of the returned `KeyValueName` class.
45
+ #
46
+ module ClassMethods
47
+ include Name::ClassMethods
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module KeyValueName
6
+ module FolderName
7
+ #
8
+ # Instance methods of a KeyValueName for a folder.
9
+ #
10
+ module InstanceMethods
11
+ include Name::InstanceMethods
12
+
13
+ def exist?
14
+ Dir.exist?(to_s)
15
+ end
16
+
17
+ def mkdir!
18
+ FileUtils.mkdir_p(to_s)
19
+ self
20
+ end
21
+
22
+ def destroy!
23
+ FileUtils.rm_rf(to_s)
24
+ end
25
+ end
26
+
27
+ #
28
+ # Class methods of the returned `KeyValueName` class.
29
+ #
30
+ module ClassMethods
31
+ include Name::ClassMethods
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+
5
+ module KeyValueName
6
+ module Name
7
+ #
8
+ # Instance method mixin for a KeyValueName.
9
+ #
10
+ module InstanceMethods
11
+ attr_accessor :parent
12
+
13
+ def to_s
14
+ result = self.class.key_value_name_spec.generate(self)
15
+ if parent
16
+ File.join(parent.to_s, result)
17
+ else
18
+ result
19
+ end
20
+ end
21
+
22
+ def <=>(other)
23
+ self.class.key_value_name_spec.compare(self, other)
24
+ end
25
+ end
26
+
27
+ #
28
+ # Class methods of the returned `KeyValueName` class.
29
+ #
30
+ module ClassMethods
31
+ attr_accessor :key_value_name_spec
32
+
33
+ def parse_to_hash(name)
34
+ key_value_name_spec.parse(name)
35
+ end
36
+
37
+ def parse(name)
38
+ new(parse_to_hash(name))
39
+ end
40
+
41
+ def glob(parent)
42
+ Dir.glob(File.join(parent.to_s, key_value_name_spec.glob)).map do |name|
43
+ basename = File.basename(name)
44
+ next unless key_value_name_spec.matches?(basename)
45
+ name = parse(basename)
46
+ name.parent = parent
47
+ name
48
+ end.compact
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ostruct'
4
+
5
+ module KeyValueName
6
+ #
7
+ # Base class for user-defined schemas.
8
+ #
9
+ class Schema
10
+ def initialize(root:)
11
+ @root = root
12
+ end
13
+
14
+ def to_s
15
+ @root
16
+ end
17
+
18
+ def parent
19
+ nil
20
+ end
21
+
22
+ def mkdir!
23
+ Dir.mkdir_p(to_s)
24
+ end
25
+ end
26
+ end