flexor 0.1.0 → 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 +30 -12
- data/CLAUDE.md +72 -0
- data/Rakefile +2 -0
- data/issues.rec +169 -0
- data/lib/f.rb +1 -0
- 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 +11 -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
|
@@ -13,10 +13,9 @@ plugins:
|
|
|
13
13
|
- rubocop-claude
|
|
14
14
|
- rubocop-performance
|
|
15
15
|
- rubocop-rspec
|
|
16
|
-
- rubocop-md
|
|
17
|
-
- rubocop-rake
|
|
18
16
|
|
|
19
17
|
AllCops:
|
|
18
|
+
NewCops: enable
|
|
20
19
|
TargetRubyVersion: 3.4
|
|
21
20
|
|
|
22
21
|
# ===========================================================================
|
|
@@ -172,9 +171,6 @@ Style/RedundantReturn:
|
|
|
172
171
|
# ===========================================================================
|
|
173
172
|
# Overrides from rubocop-performance — disable chain-hostile cops
|
|
174
173
|
# ===========================================================================
|
|
175
|
-
Performance:
|
|
176
|
-
Exclude:
|
|
177
|
-
- "spec/**/*"
|
|
178
174
|
|
|
179
175
|
# ChainArrayAllocation flags idiomatic pipelines for microsecond gains.
|
|
180
176
|
# If we need real throughput we parallelize, not uglify.
|
|
@@ -233,12 +229,19 @@ Metrics/BlockLength:
|
|
|
233
229
|
- heredoc
|
|
234
230
|
- method_call
|
|
235
231
|
AllowedMethods:
|
|
232
|
+
- command
|
|
236
233
|
- describe
|
|
237
234
|
- context
|
|
238
235
|
- shared_examples
|
|
239
236
|
- shared_examples_for
|
|
240
237
|
- shared_context
|
|
241
238
|
|
|
239
|
+
# Shared test contexts legitimately define many helpers (stdin, stdout,
|
|
240
|
+
# stderr, env, cli, exit_status, output). These are a single fixture,
|
|
241
|
+
# not independent concerns.
|
|
242
|
+
RSpec/MultipleMemoizedHelpers:
|
|
243
|
+
Max: 10
|
|
244
|
+
|
|
242
245
|
# Anonymous forwarding (*, **, &) breaks TruffleRuby, JRuby, and
|
|
243
246
|
# Ruby < 3.2. Named args are explicit and portable.
|
|
244
247
|
#
|
|
@@ -379,16 +382,31 @@ RSpec/LeadingSubject:
|
|
|
379
382
|
RSpec/ExpectChange:
|
|
380
383
|
EnforcedStyle: block
|
|
381
384
|
|
|
382
|
-
|
|
385
|
+
# Disabled because OpenStruct is used for testing
|
|
386
|
+
Style/OpenStructUse:
|
|
383
387
|
Enabled: false
|
|
384
388
|
|
|
385
|
-
|
|
386
|
-
#
|
|
387
|
-
# Behavior is very intricate here and warrants separation
|
|
388
|
-
RSpec/SpecFilePathFormat:
|
|
389
|
+
RSpec/NamedSubject:
|
|
389
390
|
Enabled: false
|
|
390
391
|
|
|
392
|
+
# Would rather be as thorough as possible here as this is inherently a "not normally safe" library
|
|
391
393
|
RSpec/NestedGroups:
|
|
392
|
-
|
|
394
|
+
Max: 4
|
|
395
|
+
|
|
393
396
|
RSpec/MultipleExpectations:
|
|
394
|
-
Max:
|
|
397
|
+
Max: 2
|
|
398
|
+
|
|
399
|
+
# Prefer the explicit &block rather than &
|
|
400
|
+
Naming/BlockForwarding:
|
|
401
|
+
Enabled: false
|
|
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
|
+
|
|
410
|
+
# This has bugs and is causing specs to fail when autocorrected
|
|
411
|
+
Performance/RedundantMerge:
|
|
412
|
+
Enabled: false
|
data/CLAUDE.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Rules
|
|
6
|
+
|
|
7
|
+
1. DO NOT edit .rubocop.yml or add inline rubocop exemptions without explicit permission
|
|
8
|
+
2. DO NOT run any git command that will rewrite history without explicit permission
|
|
9
|
+
3. PREFER method & class extraction over comments
|
|
10
|
+
4. Making new files, classes, modules, and methods IS NOT overengineering
|
|
11
|
+
5. BEFORE writing code, identify which domain concept owns the behavior. Each class and module should have a single responsibility. If the new behavior doesn't fit an existing class's responsibility, create a new one — don't expand the scope of what's already there.
|
|
12
|
+
6. DO NOT name classes with suffixes like "-er" or "-or" unless using a canonical pattern name (e.g., Parser, Router, Controller)
|
|
13
|
+
7. ALWAYS write specs first. The workflow is: identify the domain concept (rule 5), write specs describing its behavior, then implement. No implementation without a failing spec.
|
|
14
|
+
|
|
15
|
+
## Project
|
|
16
|
+
|
|
17
|
+
Flexor is a Ruby gem providing a Hash-like data store with autovivifying nested access, nil-safe chaining, and seamless conversion between hashes and method-style access. Requires Ruby >= 3.4.
|
|
18
|
+
|
|
19
|
+
## Commands
|
|
20
|
+
|
|
21
|
+
- `rake spec` — run all tests
|
|
22
|
+
- `bundle exec rspec spec/flexor/flexor_reading_spec.rb` — run a single spec file
|
|
23
|
+
- `bundle exec rspec spec/flexor/flexor_reading_spec.rb:15` — run a single example by line
|
|
24
|
+
- `rake rubocop` — run linter
|
|
25
|
+
- `bundle exec rubocop -a` — autocorrect safe offenses
|
|
26
|
+
- `rake` — run both specs and rubocop (default task)
|
|
27
|
+
- `rake benchmark` — run performance benchmarks (uses YJIT)
|
|
28
|
+
- `rake rdoc` — generate documentation
|
|
29
|
+
- `rake version:current` — show current version
|
|
30
|
+
- `rake version:bump` — bump patch version
|
|
31
|
+
- `bin/console` — IRB session with Flexor loaded
|
|
32
|
+
|
|
33
|
+
## Architecture
|
|
34
|
+
|
|
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
|
+
|
|
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`
|
|
49
|
+
|
|
50
|
+
Key internals:
|
|
51
|
+
- `@store` is the backing Hash with an autovivifying default block
|
|
52
|
+
- `@root` flag distinguishes top-level instances from auto-created children (affects `inspect` and `nil?` behavior)
|
|
53
|
+
- `method_missing` handles dynamic getter/setter; once accessed, singleton methods are cached for performance (`cache_getter`/`cache_setter`)
|
|
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
|
|
56
|
+
|
|
57
|
+
## Style Conventions (from .rubocop.yml)
|
|
58
|
+
|
|
59
|
+
- Double quotes always (`Style/StringLiterals`)
|
|
60
|
+
- No frozen_string_literal comments
|
|
61
|
+
- Trailing commas in multiline literals/arguments
|
|
62
|
+
- Pipeline-style chaining encouraged (multiline block chains allowed)
|
|
63
|
+
- Block length max 8 (keeps blocks small, push logic into pipelines)
|
|
64
|
+
- Dot-aligned chaining (`Layout/MultilineMethodCallIndentation: aligned`)
|
|
65
|
+
- Consistent 2-space argument indentation (not aligned to first arg)
|
|
66
|
+
- Explicit `begin/rescue/end` preferred over implicit method-body rescue
|
|
67
|
+
- All classes require rdoc documentation (`Style/Documentation`)
|
|
68
|
+
- RSpec: `expect { }.to change { }` block style, nested groups max 4, multiple expectations max 2
|
|
69
|
+
|
|
70
|
+
## Tests
|
|
71
|
+
|
|
72
|
+
Specs are organized by concern in `spec/flexor/` (reading, writing, merge, serialization, freeze, etc.). The top-level `spec/flexor_spec.rb` only checks the version. RSpec runs with `--format documentation` and monkey patching disabled.
|
data/Rakefile
CHANGED
data/issues.rec
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
%rec: Issue
|
|
2
|
+
%key: Id
|
|
3
|
+
%typedef: Status_t enum open in_progress closed
|
|
4
|
+
%typedef: text_t regexp /.*/
|
|
5
|
+
%type: Id uuid
|
|
6
|
+
%type: Title line
|
|
7
|
+
%type: Description text_t
|
|
8
|
+
%type: Updated date
|
|
9
|
+
%type: Status Status_t
|
|
10
|
+
%auto: Id Updated
|
|
11
|
+
|
|
12
|
+
Id: 710B6B1A-5EE5-4899-BB12-E41B44DAC090
|
|
13
|
+
Updated: Tue, 17 Mar 2026 10:15:19 -0400
|
|
14
|
+
Title: Add constant F that is set equal to Flexor
|
|
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
|
+
+ puts F[json_string].people.first.first_name
|
|
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
|
+
+ ```
|
|
97
|
+
Status: open
|
|
98
|
+
|
|
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
|
|
108
|
+
Updated: Tue, 17 Mar 2026 12:45:02 -0400
|
|
109
|
+
Title: Address rubocop offenses
|
|
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
|
+
+ ```
|
|
169
|
+
Status: open
|
data/lib/f.rb
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
F = Flexor
|
|
@@ -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
|
|
@@ -21,6 +21,7 @@ files:
|
|
|
21
21
|
- ".rspec"
|
|
22
22
|
- ".rubocop.yml"
|
|
23
23
|
- ".ruby-version"
|
|
24
|
+
- CLAUDE.md
|
|
24
25
|
- LICENSE.txt
|
|
25
26
|
- README.md
|
|
26
27
|
- Rakefile
|
|
@@ -29,14 +30,21 @@ files:
|
|
|
29
30
|
- docs/benchmark-results.md
|
|
30
31
|
- docs/original_specification.yaml
|
|
31
32
|
- docs/specification.yaml
|
|
33
|
+
- issues.rec
|
|
34
|
+
- lib/f.rb
|
|
32
35
|
- lib/flexor.rb
|
|
36
|
+
- lib/flexor/case_conversion.rb
|
|
33
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
|
|
34
43
|
- lib/flexor/serialization.rb
|
|
35
44
|
- lib/flexor/version.rb
|
|
36
45
|
- lib/flexor/vivification.rb
|
|
37
46
|
- rakelib/benchmark.rake
|
|
38
47
|
- rakelib/rdoc.rake
|
|
39
|
-
- rakelib/version.rake
|
|
40
48
|
homepage: https://github.com/gillisd/flexor
|
|
41
49
|
licenses:
|
|
42
50
|
- MIT
|
|
@@ -58,7 +66,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
58
66
|
- !ruby/object:Gem::Version
|
|
59
67
|
version: '0'
|
|
60
68
|
requirements: []
|
|
61
|
-
rubygems_version: 4.0.
|
|
69
|
+
rubygems_version: 4.0.9
|
|
62
70
|
specification_version: 4
|
|
63
71
|
summary: A Hash-like data store that does what you tell it to do
|
|
64
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
|