flexor 0.1.1 → 0.1.2
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 +4 -4
- data/.rubocop.yml +7 -0
- data/CLAUDE.md +14 -4
- data/Rakefile +2 -0
- data/issues.rec +150 -5
- data/lib/f.rb +0 -2
- data/lib/flexor/case_conversion.rb +43 -0
- data/lib/flexor/hash_delegation.rb +4 -1
- data/lib/flexor/method_dispatch.rb +49 -0
- data/lib/flexor/plugins/core.rb +153 -0
- data/lib/flexor/plugins/flex_keys.rb +61 -0
- data/lib/flexor/plugins/symbolize_keys.rb +35 -0
- data/lib/flexor/plugins.rb +38 -0
- data/lib/flexor/version.rb +1 -1
- data/lib/flexor.rb +12 -172
- metadata +8 -3
- data/rakelib/version.rake +0 -72
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4829c05a12464754b396dce1819af07f73ab4be5701bd1386bf75b17d25cf20d
|
|
4
|
+
data.tar.gz: c160abc174ce641f4b9f08fd5cdff829f64980b63282059d1181a88e8ee4aecd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7f9bcadb983ea41a9420a1bac2b4c537b24ad86ab8d515dfd9de01d3fd6c4feddab1d52a8e2afe9e82f1dfb40b233772bc30c80a243ef1543a39258413486596
|
|
7
|
+
data.tar.gz: '0858ff2f829320da4c0eefe619e5269063d649ea659d7d2c6c2b75cd3ed5adaacec74117689a6a73e104fe980c8a42f567d49706893ff3b347d6fc84436ccb4c'
|
data/.rubocop.yml
CHANGED
|
@@ -400,6 +400,13 @@ RSpec/MultipleExpectations:
|
|
|
400
400
|
Naming/BlockForwarding:
|
|
401
401
|
Enabled: false
|
|
402
402
|
|
|
403
|
+
# has_key? is a standard Ruby Hash method. Renaming it would break the
|
|
404
|
+
# public interface contract that Flexor quacks like a Hash.
|
|
405
|
+
Naming/PredicatePrefix:
|
|
406
|
+
AllowedMethods:
|
|
407
|
+
- is_a?
|
|
408
|
+
- has_key?
|
|
409
|
+
|
|
403
410
|
# This has bugs and is causing specs to fail when autocorrected
|
|
404
411
|
Performance/RedundantMerge:
|
|
405
412
|
Enabled: false
|
data/CLAUDE.md
CHANGED
|
@@ -32,17 +32,27 @@ Flexor is a Ruby gem providing a Hash-like data store with autovivifying nested
|
|
|
32
32
|
|
|
33
33
|
## Architecture
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
Plugin-based architecture following the Sequel/Roda pattern. `Flexor` class body is a minimal plugin dispatcher. All behavior lives in plugins under `Flexor::Plugins`.
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
-
|
|
39
|
-
-
|
|
37
|
+
**Plugin dispatcher** (`lib/flexor.rb`):
|
|
38
|
+
- `Flexor.plugin(mod)` — includes `mod::StoreMethods`, extends `mod::ClassMethods`
|
|
39
|
+
- `Flexor.register_plugin(:name, mod)` — symbol registration for lazy loading
|
|
40
|
+
- Lifecycle hooks: `before_load` (dependencies), `after_load` (initialization)
|
|
41
|
+
- Plugins compose via Ruby's method lookup chain — each calls `super`
|
|
42
|
+
|
|
43
|
+
**Plugins:**
|
|
44
|
+
- **`Plugins::Core`** (`lib/flexor/plugins/core.rb`) — all base store behavior: autovivifying `@store`, method-style access via `method_missing`, hash delegation, serialization (Marshal/YAML), vivification
|
|
45
|
+
- **`Plugins::FlexKeys`** (`lib/flexor/plugins/flex_keys.rb`) — overrides `[]`, `[]=`, `delete`, `key?`, `set_raw`, `deconstruct_keys`, `read_via_method` to resolve camelCase/snake_case alternates before calling `super`
|
|
46
|
+
|
|
47
|
+
**Utilities:**
|
|
48
|
+
- **`CaseConversion`** (`lib/flexor/case_conversion.rb`) — pure `module_function` utilities: `camelize`, `underscore`, `case_counterpart`
|
|
40
49
|
|
|
41
50
|
Key internals:
|
|
42
51
|
- `@store` is the backing Hash with an autovivifying default block
|
|
43
52
|
- `@root` flag distinguishes top-level instances from auto-created children (affects `inspect` and `nil?` behavior)
|
|
44
53
|
- `method_missing` handles dynamic getter/setter; once accessed, singleton methods are cached for performance (`cache_getter`/`cache_setter`)
|
|
45
54
|
- `nil?` returns `true` when `@store` is empty (non-root nodes appear nil-like until written to)
|
|
55
|
+
- Adding a new feature means adding a new file in `lib/flexor/plugins/` — no existing files modified
|
|
46
56
|
|
|
47
57
|
## Style Conventions (from .rubocop.yml)
|
|
48
58
|
|
data/Rakefile
CHANGED
data/issues.rec
CHANGED
|
@@ -1,24 +1,169 @@
|
|
|
1
1
|
%rec: Issue
|
|
2
2
|
%key: Id
|
|
3
3
|
%typedef: Status_t enum open in_progress closed
|
|
4
|
-
%typedef:
|
|
4
|
+
%typedef: text_t regexp /.*/
|
|
5
5
|
%type: Id uuid
|
|
6
6
|
%type: Title line
|
|
7
|
-
%type: Description
|
|
7
|
+
%type: Description text_t
|
|
8
8
|
%type: Updated date
|
|
9
9
|
%type: Status Status_t
|
|
10
10
|
%auto: Id Updated
|
|
11
11
|
|
|
12
|
-
Id:
|
|
12
|
+
Id: 710B6B1A-5EE5-4899-BB12-E41B44DAC090
|
|
13
13
|
Updated: Tue, 17 Mar 2026 10:15:19 -0400
|
|
14
14
|
Title: Add constant F that is set equal to Flexor
|
|
15
15
|
Description: As Flexor is built to maximize ergonomics, it would be ideal to have a shorter way to call it. In lib, there should be f.rb, where F = Flexor as the only line. It could be called like:
|
|
16
16
|
+ puts F[json_string].people.first.first_name
|
|
17
|
-
+
|
|
17
|
+
+
|
|
18
|
+
Status: closed
|
|
19
|
+
|
|
20
|
+
Id: E4B0F842-0EFD-4FA9-9841-0984A905C67B
|
|
21
|
+
Updated: Tue, 17 Mar 2026 12:45:02 -0400
|
|
22
|
+
Title: Address rubocop offenses
|
|
23
|
+
Description: Currently 30 violations as I write this
|
|
24
|
+
Status: closed
|
|
25
|
+
|
|
26
|
+
Id: 573035C3-BEDE-4C1F-9F88-3FBD4E870973
|
|
27
|
+
Updated: Tue, 17 Mar 2026 14:40:13 -0400
|
|
28
|
+
Title: Auto resolve fields against ones following a different style/casing
|
|
29
|
+
Description: It would be useful to have the ability for flexor to auto-resolve or infer fields, like:
|
|
30
|
+
+ 1. key `fooBar` is on data, user types `foo_bar`. Flexor returns value of `fooBar`
|
|
31
|
+
+ 2. Vice-versa of 1
|
|
32
|
+
+
|
|
33
|
+
+ Note that the behavior would be optional/opt-in
|
|
34
|
+
Status: closed
|
|
35
|
+
|
|
36
|
+
Id: 115686E8-6961-4B6C-9B2F-EB4DABD8A900
|
|
37
|
+
Updated: Tue, 17 Mar 2026 15:00:34 -0400
|
|
38
|
+
Title: Using F "alias" requires "require 'f'", which is undesirable
|
|
39
|
+
Description: The requirement of requiring 'f' for usage of "F" is undesirable as it pollutes global namespace, and is not intuitive for users that install flexor as the name of the gem. That alias should be loaded automatically when user does `require 'flexor'`.
|
|
40
|
+
Status: closed
|
|
41
|
+
|
|
42
|
+
Id: 19A6A0B1-D762-45D3-95E9-32545E230126
|
|
43
|
+
Updated: Mon, 30 Mar 2026 12:00:29 -0400
|
|
44
|
+
Title: Add plugin to "compile" keys into real methods
|
|
45
|
+
Description: Currently Flexor when used with IRB lacks good autocompletion. It would be beneficial to precompile the keys into methods such that they can be completed. There are also potential performance benefits to doing this. ActiveSupport::Configurable does something similar:
|
|
46
|
+
Status: open
|
|
47
|
+
|
|
48
|
+
Id: 1FC0E609-B57F-45C3-9305-EB47601B259E
|
|
49
|
+
Updated: Mon, 30 Mar 2026 12:01:05 -0400
|
|
50
|
+
Title: ActiveSupport::Configurable example (tied to previous ticket):
|
|
51
|
+
Description: 4 │ module ActiveSupport
|
|
52
|
+
+ 5 │ # = Active Support \Configurable
|
|
53
|
+
+ 6 │ # Configurable provides a <tt>config</tt> method to store and retrieve
|
|
54
|
+
+ 7 │ # configuration options as an OrderedOptions.
|
|
55
|
+
+ 8 │ module Configurable
|
|
56
|
+
+ 9 │ extend ActiveSupport::Concern
|
|
57
|
+
+ 10 │ class Configuration < ActiveSupport::InheritableOptions
|
|
58
|
+
+ 11 │ def compile_methods\\\\\\\\\\\\!
|
|
59
|
+
+ 12 │ self.class.compile_methods\\\\\\\\\\\\!(keys)
|
|
60
|
+
+ 13 │ end
|
|
61
|
+
+ 14 │ # Compiles reader methods so we don't have to go through method_missing.
|
|
62
|
+
+ 15 │ def self.compile_methods\\\\\\\\\\\\!(keys)
|
|
63
|
+
+ 16 │ keys.reject { |m| method_defined?(m) }.each do |key|
|
|
64
|
+
+ 17 │ class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
|
65
|
+
+ 18 │ def #{key}; _get(#{key.inspect}); end
|
|
66
|
+
+ 19 │ RUBY
|
|
67
|
+
+ 20 │ end
|
|
68
|
+
+ 21 │ end
|
|
69
|
+
+ 22 │ end
|
|
70
|
+
Status: open
|
|
71
|
+
|
|
72
|
+
Id: 3B49DC55-C580-42C7-AB0D-08F0CB899B2D
|
|
73
|
+
Updated: Sat, 11 Apr 2026 13:30:31 -0400
|
|
74
|
+
Title: Create plugin to allow for the "?" method suffix
|
|
75
|
+
Description: For example, when doing something like
|
|
76
|
+
+ ```ruby
|
|
77
|
+
+ object = Flexor[mandatory: true]
|
|
78
|
+
+
|
|
79
|
+
+ # CURRENT BEHAVIOR:
|
|
80
|
+
+ #
|
|
81
|
+
+ object.mandatory
|
|
82
|
+
+ # => true
|
|
83
|
+
+ #
|
|
84
|
+
+ object.mandatory?
|
|
85
|
+
+ # => nil
|
|
86
|
+
+ #
|
|
87
|
+
+
|
|
88
|
+
+ # DESIRED BEHAVIOR:
|
|
89
|
+
+ #
|
|
90
|
+
+ object.mandatory
|
|
91
|
+
+ # => true
|
|
92
|
+
+ #
|
|
93
|
+
+ object.mandatory?
|
|
94
|
+
+ # => true
|
|
95
|
+
+ #
|
|
96
|
+
+ ```
|
|
18
97
|
Status: open
|
|
19
98
|
|
|
20
|
-
Id:
|
|
99
|
+
Id: B0673B2E-A2A2-4FBE-BBF9-F41FA1CA3169
|
|
100
|
+
Updated: Tue, 17 Mar 2026 10:15:19 -0400
|
|
101
|
+
Title: Add constant F that is set equal to Flexor
|
|
102
|
+
Description: As Flexor is built to maximize ergonomics, it would be ideal to have a shorter way to call it. In lib, there should be f.rb, where F = Flexor as the only line. It could be called like:
|
|
103
|
+
+ puts F[json_string].people.first.first_name
|
|
104
|
+
+
|
|
105
|
+
Status: closed
|
|
106
|
+
|
|
107
|
+
Id: 9938B05E-15F6-402B-AF42-7046D5B5F671
|
|
21
108
|
Updated: Tue, 17 Mar 2026 12:45:02 -0400
|
|
22
109
|
Title: Address rubocop offenses
|
|
23
110
|
Description: Currently 30 violations as I write this
|
|
111
|
+
Status: closed
|
|
112
|
+
|
|
113
|
+
Id: 4BAD9D04-F360-4859-8454-B40CB80F0F85
|
|
114
|
+
Updated: Tue, 17 Mar 2026 14:40:13 -0400
|
|
115
|
+
Title: Auto resolve fields against ones following a different style/casing
|
|
116
|
+
Description: It would be useful to have the ability for flexor to auto-resolve or infer fields, like:
|
|
117
|
+
+ 1. key `fooBar` is on data, user types `foo_bar`. Flexor returns value of `fooBar`
|
|
118
|
+
+ 2. Vice-versa of 1
|
|
119
|
+
+
|
|
120
|
+
+ Note that the behavior would be optional/opt-in
|
|
121
|
+
Status: closed
|
|
122
|
+
|
|
123
|
+
Id: B85383C2-5142-464F-81CF-7938445411E1
|
|
124
|
+
Updated: Tue, 17 Mar 2026 15:00:34 -0400
|
|
125
|
+
Title: Using F "alias" requires "require 'f'", which is undesirable
|
|
126
|
+
Description: The requirement of requiring 'f' for usage of "F" is undesirable as it pollutes global namespace, and is not intuitive for users that install flexor as the name of the gem. That alias should be loaded automatically when user does `require 'flexor'`.
|
|
127
|
+
Status: closed
|
|
128
|
+
|
|
129
|
+
Id: 67833956-1397-4820-887B-60EE975CDF46
|
|
130
|
+
Updated: Sat, 21 Mar 2026 22:38:19 -0400
|
|
131
|
+
Title: Perform spike around IRB autocompletions
|
|
132
|
+
Description: IRB autocompletions are incredibly useful, but flexor only exposes its own internal methods, rather than the fields for autocompletions. Some thoughts:
|
|
133
|
+
+ 1. Method caching is already (or was) built into flexor - if methods get defined, IRB can cache them
|
|
134
|
+
+ 2. Alternatively but more robustly, flexor could dynamically generate RBS types for the RBS type infer completor.
|
|
135
|
+
+ 3. Either which way, the plugin system allows us to isolate this behvior and only trigger it when IRB is active
|
|
136
|
+
Status: open
|
|
137
|
+
|
|
138
|
+
Id: 3ED3C15C-4051-11F1-9F71-FE6CB9572C2F
|
|
139
|
+
Updated: Fri, 24 Apr 2026 22:48:31 -0400
|
|
140
|
+
Title: Conflicting nil behavior between ||=, implicit nil, and ".nil?"
|
|
141
|
+
Description: #Write a spec/test for the following example:
|
|
142
|
+
+ ```ruby
|
|
143
|
+
+ context 'when flexor has no key set' do
|
|
144
|
+
+ describe 'setting via ||=' do
|
|
145
|
+
+ it 'sets the attribute' do
|
|
146
|
+
+ f = Flexor.new
|
|
147
|
+
+ f[:foo] ||= :bar
|
|
148
|
+
+ expect(f.foo).to eq(:bar)
|
|
149
|
+
+ end
|
|
150
|
+
+ end
|
|
151
|
+
+
|
|
152
|
+
+ describe 'setting via implicit nil OR' do
|
|
153
|
+
+ it 'correctly sets the attribute' do
|
|
154
|
+
+ f = Flexor.new
|
|
155
|
+
+ f[:foo] || f[:foo] = :bar
|
|
156
|
+
+ expect(f.foo).to eq(:bar)
|
|
157
|
+
+ end
|
|
158
|
+
+ end
|
|
159
|
+
+
|
|
160
|
+
+ describe 'setting via nil? OR' do
|
|
161
|
+
+ it 'correctly sets the attribute' do
|
|
162
|
+
+ f = Flexor.new
|
|
163
|
+
+ f[:foo].nil? || f[:foo] = :bar
|
|
164
|
+
+ expect(f.foo).to eq(:bar)
|
|
165
|
+
+ end
|
|
166
|
+
+ end
|
|
167
|
+
+ end
|
|
168
|
+
+ ```
|
|
24
169
|
Status: open
|
data/lib/f.rb
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
class Flexor
|
|
2
|
+
##
|
|
3
|
+
# Pure-function utilities for converting between camelCase and snake_case.
|
|
4
|
+
# Used by the FlexKeys plugin to compute alternate key forms.
|
|
5
|
+
module CaseConversion
|
|
6
|
+
CAMEL_BOUNDARY = /(?<=[A-Z])(?=[A-Z][a-z])|(?<=[a-z\d])(?=[A-Z])/
|
|
7
|
+
UNDERSCORE_SEGMENT = %r{(?:_|(/))([a-z\d]*)}
|
|
8
|
+
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def camelize(term)
|
|
12
|
+
string = term.to_s.dup
|
|
13
|
+
return string if string.empty?
|
|
14
|
+
|
|
15
|
+
string.gsub!(UNDERSCORE_SEGMENT) do
|
|
16
|
+
"#{Regexp.last_match(1) && "::"}#{Regexp.last_match(2).capitalize}"
|
|
17
|
+
end
|
|
18
|
+
return string if string.empty?
|
|
19
|
+
|
|
20
|
+
string[0] = string[0].downcase
|
|
21
|
+
string
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def underscore(camel_cased_word)
|
|
25
|
+
return camel_cased_word.to_s.dup unless /[A-Z-]/.match?(camel_cased_word)
|
|
26
|
+
|
|
27
|
+
word = camel_cased_word.to_s.dup
|
|
28
|
+
word.gsub!(CAMEL_BOUNDARY, "_")
|
|
29
|
+
word.tr!("-", "_")
|
|
30
|
+
word.downcase!
|
|
31
|
+
word
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def case_counterpart(key)
|
|
35
|
+
str = key.to_s
|
|
36
|
+
if str.include?("_")
|
|
37
|
+
camelize(str).to_sym
|
|
38
|
+
elsif str.match?(/[A-Z]/)
|
|
39
|
+
underscore(str).to_sym
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
class Flexor
|
|
2
|
+
##
|
|
3
|
+
# Handles dynamic getter/setter dispatch via method_missing and
|
|
4
|
+
# caches singleton methods for repeated access.
|
|
5
|
+
module MethodDispatch
|
|
6
|
+
def respond_to_missing?(_name, _include_private = false)
|
|
7
|
+
true
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def method_missing(name, *args, &block)
|
|
13
|
+
return super if block
|
|
14
|
+
|
|
15
|
+
case [name, args]
|
|
16
|
+
in /^[^=]+=$/, [arg] then write_via_method(name, arg)
|
|
17
|
+
in _, [] then read_via_method(name)
|
|
18
|
+
else super
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def write_via_method(name, arg)
|
|
23
|
+
key = name.to_s.chomp("=").to_sym
|
|
24
|
+
cache_setter(name, key)
|
|
25
|
+
self[key] = arg
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def read_via_method(name)
|
|
29
|
+
cache_getter(name) if !frozen? && @store.key?(name)
|
|
30
|
+
self[name]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def cache_setter(name, key)
|
|
34
|
+
define_singleton_method(name) do |val = nil, &blk|
|
|
35
|
+
raise NoMethodError, "undefined method '#{name}' for #{inspect}" if blk
|
|
36
|
+
|
|
37
|
+
self[key] = val
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def cache_getter(name)
|
|
42
|
+
define_singleton_method(name) do |*a, &blk|
|
|
43
|
+
raise NoMethodError, "undefined method '#{name}' for #{inspect}" if blk || !a.empty?
|
|
44
|
+
|
|
45
|
+
self[name]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
class Flexor
|
|
2
|
+
module Plugins
|
|
3
|
+
##
|
|
4
|
+
# Core plugin providing all fundamental Flexor behavior.
|
|
5
|
+
# Bundles Vivification, HashDelegation, Serialization, and
|
|
6
|
+
# MethodDispatch along with every instance and class method
|
|
7
|
+
# that ships with a default Flexor install.
|
|
8
|
+
module Core
|
|
9
|
+
##
|
|
10
|
+
# Instance methods for the core Flexor data store.
|
|
11
|
+
module StoreMethods
|
|
12
|
+
include Flexor::Vivification
|
|
13
|
+
include Flexor::HashDelegation
|
|
14
|
+
include Flexor::Serialization
|
|
15
|
+
include Flexor::MethodDispatch
|
|
16
|
+
|
|
17
|
+
def initialize(hash = {}, root: true)
|
|
18
|
+
raise ArgumentError, "expected a Hash, got #{hash.class}" unless hash.is_a?(Hash)
|
|
19
|
+
|
|
20
|
+
@root = root
|
|
21
|
+
@store = vivify(hash)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def initialize_copy(original)
|
|
25
|
+
super
|
|
26
|
+
@store = @store.dup
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def [](key)
|
|
30
|
+
@store[key]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def []=(key, value)
|
|
34
|
+
@store[key] = vivify_value(value)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def set_raw(key, value)
|
|
38
|
+
@store[key] = value
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def delete(key)
|
|
42
|
+
@store.delete(key)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def clear
|
|
46
|
+
@store.clear
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_ary
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def freeze
|
|
55
|
+
@store.freeze
|
|
56
|
+
super
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def to_h
|
|
60
|
+
@store.each_with_object({}) do |(key, value), hash|
|
|
61
|
+
result = recurse_to_h(value)
|
|
62
|
+
hash[key] = result unless value.is_a?(Flexor) && result.nil?
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def to_json(...)
|
|
67
|
+
require "json"
|
|
68
|
+
to_h.to_json(...)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def to_s
|
|
72
|
+
return "" if nil?
|
|
73
|
+
|
|
74
|
+
@store.to_s
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def inspect
|
|
78
|
+
return @store.inspect if @root
|
|
79
|
+
return nil.inspect if @store.empty?
|
|
80
|
+
|
|
81
|
+
@store.inspect
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def deconstruct
|
|
85
|
+
@store.values
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def deconstruct_keys(keys)
|
|
89
|
+
return @store if keys.nil?
|
|
90
|
+
|
|
91
|
+
@store.slice(*keys)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def nil?
|
|
95
|
+
@store.empty?
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def merge!(other)
|
|
99
|
+
other = other.to_h if other.is_a?(Flexor)
|
|
100
|
+
other.each do |key, value|
|
|
101
|
+
if value.is_a?(Hash) && self[key].is_a?(Flexor) && !self[key].nil?
|
|
102
|
+
self[key].merge!(value)
|
|
103
|
+
else
|
|
104
|
+
self[key] = value
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
self
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def merge(other)
|
|
111
|
+
dup.merge!(other)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def ==(other)
|
|
115
|
+
case other
|
|
116
|
+
in nil then nil?
|
|
117
|
+
in Flexor then to_h == other.to_h
|
|
118
|
+
in Hash then to_h == other
|
|
119
|
+
else super
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def ===(other)
|
|
124
|
+
other.nil? ? nil? : super
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
##
|
|
129
|
+
# Class-level methods for the core Flexor data store.
|
|
130
|
+
module ClassMethods
|
|
131
|
+
def [](input = {})
|
|
132
|
+
case input
|
|
133
|
+
when String then from_json(input)
|
|
134
|
+
when Hash then new(input)
|
|
135
|
+
else raise ArgumentError, "expected a String or Hash, got #{input.class}"
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def from_json(json)
|
|
140
|
+
require "json"
|
|
141
|
+
JSON.parse(json, symbolize_names: true)
|
|
142
|
+
.then { new it }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def ===(other)
|
|
146
|
+
other.is_a?(self)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
Flexor.register_plugin(:core, self)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
class Flexor
|
|
2
|
+
module Plugins
|
|
3
|
+
##
|
|
4
|
+
# CamelCase/snake_case key resolution plugin. Overrides key-accepting
|
|
5
|
+
# methods to check for an alternate-case match when the exact key is
|
|
6
|
+
# not found. Composes via +super+ with Core (or any plugin below it).
|
|
7
|
+
module FlexKeys
|
|
8
|
+
##
|
|
9
|
+
# Instance methods that resolve symbol keys to their alternate-case
|
|
10
|
+
# counterpart before delegating to the underlying store.
|
|
11
|
+
module StoreMethods
|
|
12
|
+
def [](key)
|
|
13
|
+
super(resolve_flex_key(key))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def []=(key, value)
|
|
17
|
+
super(resolve_flex_key(key), value)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def set_raw(key, value)
|
|
21
|
+
super(resolve_flex_key(key), value)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def delete(key)
|
|
25
|
+
super(resolve_flex_key(key))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def key?(key)
|
|
29
|
+
super(resolve_flex_key(key))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def deconstruct_keys(keys)
|
|
33
|
+
return super if keys.nil?
|
|
34
|
+
|
|
35
|
+
keys.each_with_object({}) do |key, hash|
|
|
36
|
+
resolved = resolve_flex_key(key)
|
|
37
|
+
hash[key] = @store[resolved] if @store.key?(resolved)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def resolve_flex_key(key)
|
|
44
|
+
return key if @store.key?(key)
|
|
45
|
+
return key unless key.is_a?(Symbol)
|
|
46
|
+
|
|
47
|
+
alt = CaseConversion.case_counterpart(key)
|
|
48
|
+
alt && @store.key?(alt) ? alt : key
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def read_via_method(name)
|
|
52
|
+
resolved = resolve_flex_key(name)
|
|
53
|
+
cache_getter(name) if !frozen? && @store.key?(resolved)
|
|
54
|
+
self[name]
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
Flexor.register_plugin(:flex_keys, self)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
class Flexor
|
|
2
|
+
module Plugins
|
|
3
|
+
##
|
|
4
|
+
# Normalizes string keys to symbols at ingestion and access time.
|
|
5
|
+
# Ensures that data arriving with string keys (from JSON, YAML, or
|
|
6
|
+
# plain Ruby hashes) is accessible via method and symbol-bracket
|
|
7
|
+
# access without manual conversion.
|
|
8
|
+
module SymbolizeKeys
|
|
9
|
+
##
|
|
10
|
+
# Instance methods that convert string keys to symbols on
|
|
11
|
+
# ingestion (+vivify+, +[]=+) and read (+[]+).
|
|
12
|
+
module StoreMethods
|
|
13
|
+
def [](key)
|
|
14
|
+
super(symbolize_key(key))
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def []=(key, value)
|
|
18
|
+
super(symbolize_key(key), value)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def symbolize_key(key)
|
|
24
|
+
key.is_a?(String) ? key.to_sym : key
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def vivify(hash)
|
|
28
|
+
super(hash.transform_keys { |k| symbolize_key(k) })
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
Flexor.register_plugin(:symbolize_keys, self)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
class Flexor
|
|
2
|
+
##
|
|
3
|
+
# Namespace for Flexor plugins. Each plugin is a module containing
|
|
4
|
+
# optional +StoreMethods+ and +ClassMethods+ submodules.
|
|
5
|
+
module Plugins
|
|
6
|
+
##
|
|
7
|
+
# Class-level methods for registering and loading plugins.
|
|
8
|
+
# Extended onto Flexor to provide +.plugin+ and +.register_plugin+.
|
|
9
|
+
module Dispatcher
|
|
10
|
+
def register_plugin(symbol, mod)
|
|
11
|
+
Flexor.plugin_registry[symbol] = mod
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def plugin(mod, ...)
|
|
15
|
+
mod = resolve_plugin(mod)
|
|
16
|
+
mod.before_load(self, ...) if mod.respond_to?(:before_load)
|
|
17
|
+
|
|
18
|
+
include(mod::StoreMethods) if defined?(mod::StoreMethods)
|
|
19
|
+
extend(mod::ClassMethods) if defined?(mod::ClassMethods)
|
|
20
|
+
|
|
21
|
+
mod.after_load(self, ...) if mod.respond_to?(:after_load)
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def plugin_registry
|
|
26
|
+
@plugin_registry ||= {}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def resolve_plugin(mod)
|
|
32
|
+
return mod unless mod.is_a?(Symbol)
|
|
33
|
+
|
|
34
|
+
Flexor.plugin_registry.fetch(mod)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/flexor/version.rb
CHANGED
data/lib/flexor.rb
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
require_relative "flexor/version"
|
|
2
|
+
require_relative "flexor/plugins"
|
|
2
3
|
require_relative "flexor/hash_delegation"
|
|
3
4
|
require_relative "flexor/serialization"
|
|
4
5
|
require_relative "flexor/vivification"
|
|
6
|
+
require_relative "flexor/method_dispatch"
|
|
7
|
+
require_relative "flexor/case_conversion"
|
|
5
8
|
|
|
6
9
|
##
|
|
7
10
|
# A Hash-like data store with autovivifying nested access, nil-safe
|
|
@@ -10,178 +13,15 @@ require_relative "flexor/vivification"
|
|
|
10
13
|
class Flexor
|
|
11
14
|
class Error < StandardError; end
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
include Vivification
|
|
16
|
-
|
|
17
|
-
def self.[](input = {})
|
|
18
|
-
case input
|
|
19
|
-
when String then from_json(input)
|
|
20
|
-
when Hash then new(input)
|
|
21
|
-
else raise ArgumentError, "expected a String or Hash, got #{input.class}"
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def self.from_json(json)
|
|
26
|
-
require "json"
|
|
27
|
-
JSON.parse(json, symbolize_names: true)
|
|
28
|
-
.then { new it }
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def self.===(other)
|
|
32
|
-
other.is_a?(self)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def initialize(hash = {}, root: true)
|
|
36
|
-
raise ArgumentError, "expected a Hash, got #{hash.class}" unless hash.is_a?(Hash)
|
|
37
|
-
|
|
38
|
-
@root = root
|
|
39
|
-
@store = vivify(hash)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def initialize_copy(original)
|
|
43
|
-
super
|
|
44
|
-
@store = @store.dup
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def [](key)
|
|
48
|
-
@store[key]
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def []=(key, value)
|
|
52
|
-
@store[key] = vivify_value(value)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def set_raw(key, value)
|
|
56
|
-
@store[key] = value
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def delete(key)
|
|
60
|
-
@store.delete(key)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def clear
|
|
64
|
-
@store.clear
|
|
65
|
-
self
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def to_ary
|
|
69
|
-
nil
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def freeze
|
|
73
|
-
@store.freeze
|
|
74
|
-
super
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def to_h
|
|
78
|
-
@store.each_with_object({}) do |(key, value), hash|
|
|
79
|
-
result = recurse_to_h(value)
|
|
80
|
-
hash[key] = result unless value.is_a?(Flexor) && result.nil?
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
def to_json(...)
|
|
85
|
-
require "json"
|
|
86
|
-
to_h.to_json(...)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def to_s
|
|
90
|
-
return "" if nil?
|
|
91
|
-
|
|
92
|
-
@store.to_s
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def inspect
|
|
96
|
-
return @store.inspect if @root
|
|
97
|
-
return nil.inspect if @store.empty?
|
|
98
|
-
|
|
99
|
-
@store.inspect
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def deconstruct
|
|
103
|
-
@store.values
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def deconstruct_keys(keys)
|
|
107
|
-
return @store if keys.nil?
|
|
108
|
-
|
|
109
|
-
@store.slice(*keys)
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def nil?
|
|
113
|
-
@store.empty?
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def merge!(other)
|
|
117
|
-
other = other.to_h if other.is_a?(Flexor)
|
|
118
|
-
other.each do |key, value|
|
|
119
|
-
if value.is_a?(Hash) && self[key].is_a?(Flexor) && !self[key].nil?
|
|
120
|
-
self[key].merge!(value)
|
|
121
|
-
else
|
|
122
|
-
self[key] = value
|
|
123
|
-
end
|
|
124
|
-
end
|
|
125
|
-
self
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def merge(other)
|
|
129
|
-
dup.merge!(other)
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def ==(other)
|
|
133
|
-
case other
|
|
134
|
-
in nil then nil?
|
|
135
|
-
in Flexor then to_h == other.to_h
|
|
136
|
-
in Hash then to_h == other
|
|
137
|
-
else super
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
def ===(other)
|
|
142
|
-
other.nil? ? nil? : super
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def respond_to_missing?(_name, _include_private = false)
|
|
146
|
-
true
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
private
|
|
150
|
-
|
|
151
|
-
def method_missing(name, *args, &block)
|
|
152
|
-
return super if block
|
|
153
|
-
|
|
154
|
-
case [name, args]
|
|
155
|
-
in /^[^=]+=$/, [arg] then write_via_method(name, arg)
|
|
156
|
-
in _, [] then read_via_method(name)
|
|
157
|
-
else super
|
|
158
|
-
end
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def write_via_method(name, arg)
|
|
162
|
-
key = name.to_s.chomp("=").to_sym
|
|
163
|
-
cache_setter(name, key)
|
|
164
|
-
self[key] = arg
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
def read_via_method(name)
|
|
168
|
-
cache_getter(name) if !frozen? && @store.key?(name)
|
|
169
|
-
self[name]
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def cache_setter(name, key)
|
|
173
|
-
define_singleton_method(name) do |val = nil, &blk|
|
|
174
|
-
raise NoMethodError, "undefined method '#{name}' for #{inspect}" if blk
|
|
16
|
+
extend Plugins::Dispatcher
|
|
17
|
+
end
|
|
175
18
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
19
|
+
require_relative "flexor/plugins/core"
|
|
20
|
+
require_relative "flexor/plugins/symbolize_keys"
|
|
21
|
+
require_relative "flexor/plugins/flex_keys"
|
|
179
22
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
23
|
+
Flexor.plugin(:core)
|
|
24
|
+
Flexor.plugin(:flex_keys)
|
|
25
|
+
Flexor.plugin(:symbolize_keys)
|
|
183
26
|
|
|
184
|
-
|
|
185
|
-
end
|
|
186
|
-
end
|
|
187
|
-
end
|
|
27
|
+
require_relative "f"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: flexor
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Gillis
|
|
@@ -33,13 +33,18 @@ files:
|
|
|
33
33
|
- issues.rec
|
|
34
34
|
- lib/f.rb
|
|
35
35
|
- lib/flexor.rb
|
|
36
|
+
- lib/flexor/case_conversion.rb
|
|
36
37
|
- lib/flexor/hash_delegation.rb
|
|
38
|
+
- lib/flexor/method_dispatch.rb
|
|
39
|
+
- lib/flexor/plugins.rb
|
|
40
|
+
- lib/flexor/plugins/core.rb
|
|
41
|
+
- lib/flexor/plugins/flex_keys.rb
|
|
42
|
+
- lib/flexor/plugins/symbolize_keys.rb
|
|
37
43
|
- lib/flexor/serialization.rb
|
|
38
44
|
- lib/flexor/version.rb
|
|
39
45
|
- lib/flexor/vivification.rb
|
|
40
46
|
- rakelib/benchmark.rake
|
|
41
47
|
- rakelib/rdoc.rake
|
|
42
|
-
- rakelib/version.rake
|
|
43
48
|
homepage: https://github.com/gillisd/flexor
|
|
44
49
|
licenses:
|
|
45
50
|
- MIT
|
|
@@ -61,7 +66,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
61
66
|
- !ruby/object:Gem::Version
|
|
62
67
|
version: '0'
|
|
63
68
|
requirements: []
|
|
64
|
-
rubygems_version: 4.0.
|
|
69
|
+
rubygems_version: 4.0.9
|
|
65
70
|
specification_version: 4
|
|
66
71
|
summary: A Hash-like data store that does what you tell it to do
|
|
67
72
|
test_files: []
|
data/rakelib/version.rake
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
VERSION_PATTERN = /VERSION\s*=\s*"(\d+\.\d+\.\d+)"/
|
|
2
|
-
|
|
3
|
-
# Encapsulates version file manipulation logic for rake tasks.
|
|
4
|
-
module VersionBumper
|
|
5
|
-
module_function
|
|
6
|
-
|
|
7
|
-
def version_path
|
|
8
|
-
File.expand_path("../lib/flexor/version.rb", __dir__)
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def print_current
|
|
12
|
-
require_relative "../lib/flexor/version"
|
|
13
|
-
puts "Current version: #{Flexor::VERSION}"
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def bump
|
|
17
|
-
File.open(version_path, File::RDWR, 0o644) do |f|
|
|
18
|
-
f.flock(File::LOCK_EX)
|
|
19
|
-
old_version, new_version, new_source = compute_bump(f.read)
|
|
20
|
-
f.rewind
|
|
21
|
-
f.write(new_source)
|
|
22
|
-
f.truncate(f.pos)
|
|
23
|
-
puts "Version bumped from #{old_version} to #{new_version}"
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def compute_bump(source)
|
|
28
|
-
match = source.match(VERSION_PATTERN)
|
|
29
|
-
abort "Could not find VERSION in #{version_path}" unless match
|
|
30
|
-
|
|
31
|
-
old_version = match[1]
|
|
32
|
-
parts = old_version.split(".").map(&:to_i)
|
|
33
|
-
parts[-1] += 1
|
|
34
|
-
new_version = parts.join(".")
|
|
35
|
-
new_source = source.sub(/VERSION\s*=\s*"#{Regexp.escape(old_version)}"/, "VERSION = \"#{new_version}\"")
|
|
36
|
-
[old_version, new_version, new_source]
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def commit
|
|
40
|
-
require_relative "../lib/flexor/version"
|
|
41
|
-
system("git", "add", version_path) || abort("git add failed")
|
|
42
|
-
system("git", "commit", "-m", "Bump version to #{Flexor::VERSION}") || abort("git commit failed")
|
|
43
|
-
puts "Version change committed."
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def revert
|
|
47
|
-
last_message = `git log -1 --pretty=%B`.strip
|
|
48
|
-
abort "Last commit does not appear to be a version bump." unless last_message.start_with?("Bump version to ")
|
|
49
|
-
|
|
50
|
-
system("git", "revert", "HEAD", "--no-edit") || abort("git revert failed")
|
|
51
|
-
puts "Version bump reverted."
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
namespace :version do
|
|
56
|
-
desc "Display the current version"
|
|
57
|
-
task(:current) { VersionBumper.print_current }
|
|
58
|
-
|
|
59
|
-
desc "Bump the patch version"
|
|
60
|
-
task(:bump) { VersionBumper.bump }
|
|
61
|
-
|
|
62
|
-
desc "Commit the version change"
|
|
63
|
-
task(:commit) { VersionBumper.commit }
|
|
64
|
-
|
|
65
|
-
desc "Revert the last version bump commit"
|
|
66
|
-
task(:revert) { VersionBumper.revert }
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
namespace :release do
|
|
70
|
-
desc "Bump version, commit, and release"
|
|
71
|
-
task full: ["version:bump", "version:commit", :release]
|
|
72
|
-
end
|