data_for 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 70b639bf977cd804d0b23908514220947ca35736f8042019f9bd02fb4e11e911
4
+ data.tar.gz: 4f48be5128f8d9204217cba84cda4e4d1fd0b32ad4900421c27f339b80e51d0a
5
+ SHA512:
6
+ metadata.gz: b6edc2f15523421111119c2301e83b39d9776e366ed9aeca4fff5b91b3fbe9574028cfe006c97ceb914b9cf0a9bbe9c9e9c0119c9b088663e9cc563e303c768b
7
+ data.tar.gz: '0593d66e096c6681695f1aad9ed8ad79e8fe9958eefd6ba74613cc3cbd89094f062bdf2ebc679510ad5dd9ef0182bed1f8e4385aba8552e517670ff5f85dbe47'
data/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ ## Changelog
2
+
3
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
4
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
5
+
6
+ ## [Unreleased]
7
+
8
+ Initial pre-release. The gem has not yet been published to RubyGems.
9
+
10
+ ### Added
11
+
12
+ - `DataFor::Model` concern for read-only `Data.define` models backed by Rails config files.
13
+ - `find`, `find!`, `find_by`, `find_by!`, and `where` query methods.
14
+ - O(1) primary-key lookup for `find` and `find!` via a lazily-built index.
15
+ - `DataFor::RecordNotFound` raised by bang variants.
16
+ - `self.primary_key=` for non-`:id` primary keys.
17
+ - `cast_<member>` hooks for typed attribute casting (including nested Data models).
18
+ - `project:` keyword on `config` for reshaping the source data before it becomes the model's record set, letting a single YAML file drive multiple query surfaces.
19
+ - `loader:` keyword on `config` for loading source data outside of Rails.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright Andy Cohen
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,273 @@
1
+ # DataFor
2
+
3
+ > Queryable, read-only Ruby `Data` models backed by Rails config files.
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/data_for.svg)](https://rubygems.org/gems/data_for)
6
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.4-ruby.svg)](https://www.ruby-lang.org/)
7
+
8
+ ```ruby
9
+ Country = Data.define(:id, :name) do
10
+ include DataFor::Model
11
+ config :countries
12
+ end
13
+
14
+ Country.find("US") #=> #<data Country id="US", name="United States">
15
+ Country.find!("ZZ") # raises DataFor::RecordNotFound
16
+ Country.find_by(name: "Canada") #=> #<data Country id="CA", name="Canada">
17
+ Country.where(name: "Canada") #=> [#<data Country id="CA", name="Canada">]
18
+ ```
19
+
20
+ `DataFor` turns YAML in your Rails config directory into queryable, read-only
21
+ models — countries, currencies, plans, and other reference data that rarely
22
+ changes and doesn't belong in your database. It's built on Ruby's native
23
+ [`Data.define`](https://docs.ruby-lang.org/en/3.4/Data.html) and Rails'
24
+ [`config_for`](https://guides.rubyonrails.org/configuring.html#config-for-loading-external-configuration), so the records are immutable value objects and the
25
+ storage is a YAML file you already know how to edit.
26
+
27
+ ## Why?
28
+
29
+ Not all of an application's data must live in the database. Reference data —
30
+ countries, currencies, subscription tiers, feature flags, lookup tables — has
31
+ a few properties that distinguish it from "real" data:
32
+
33
+ - It rarely changes (changes are deployments, not user actions).
34
+ - It is not user-editable.
35
+ - It needs to be versioned alongside the code that depends on it.
36
+ - It is small enough to live in memory.
37
+
38
+ ### Database vs. `DataFor`
39
+
40
+ | | Database | `DataFor` |
41
+ |---------------------------------|----------|----------------------|
42
+ | Writeable at runtime | yes | no |
43
+ | Schema migrations required | yes | no |
44
+ | Versioned with application code | no | yes |
45
+ | Joins | yes | no |
46
+ | Editable without a redeploy | yes | no |
47
+ | Cost per query | network | in-memory hash/array |
48
+
49
+ If your data is on the right-hand side of that table, `DataFor` is probably a
50
+ better fit than a table.
51
+
52
+ ## Installation
53
+
54
+ Add to your `Gemfile`:
55
+
56
+ ```ruby
57
+ gem "data_for"
58
+ ```
59
+
60
+ Then:
61
+
62
+ ```bash
63
+ bundle install
64
+ ```
65
+
66
+ `DataFor` requires Ruby `>= 3.4` (for the implicit `it` block parameter) and
67
+ Rails `>= 6.1` (for `config_for`).
68
+
69
+ ## Usage
70
+
71
+ ### 1. Put your data in `config/`
72
+
73
+ Rails' `config_for` reads `config/<name>.yml` files. The top-level key
74
+ `shared` provides defaults across environments; per-environment keys
75
+ (`development`, `production`, etc.) override them.
76
+
77
+ Example `config/countries.yml`:
78
+
79
+ ```yaml
80
+ shared:
81
+ - id: AU
82
+ name: Australia
83
+ states:
84
+ - { id: ACT, name: "Australian Capital Territory", country_id: AU }
85
+ - { id: NSW, name: "New South Wales", country_id: AU }
86
+ # ...
87
+ - id: CA
88
+ name: Canada
89
+ states:
90
+ - { id: AB, name: Alberta, country_id: CA }
91
+ # ...
92
+ - id: US
93
+ name: "United States"
94
+ states:
95
+ - { id: AL, name: Alabama, country_id: US }
96
+ # ...
97
+ ```
98
+
99
+ ### 2. Define a model
100
+
101
+ Use Ruby's `Data.define` to declare the value object's members, then
102
+ `include DataFor::Model` to make it queryable:
103
+
104
+ ```ruby
105
+ Country = Data.define(:id, :name, :states) do
106
+ include DataFor::Model
107
+ config :countries
108
+ end
109
+ ```
110
+
111
+ ### 3. Query
112
+
113
+ ```ruby
114
+ Country.find("AU")
115
+ # => #<data Country id="AU", name="Australia", states=[...]>
116
+
117
+ Country.find!("ZZ")
118
+ # raises DataFor::RecordNotFound
119
+
120
+ Country.find_by(name: "United States")
121
+ # => #<data Country id="US", name="United States", states=[...]>
122
+
123
+ Country.find_by!(name: "Atlantis")
124
+ # raises DataFor::RecordNotFound
125
+
126
+ Country.where(name: "Canada")
127
+ # => [#<data Country id="CA", ...>]
128
+
129
+ Country.all
130
+ # => [#<data Country id="AU", ...>, #<data Country id="CA", ...>, ...]
131
+ ```
132
+
133
+ `find` uses a primary-key index and is O(1). `find_by` and `where` scan
134
+ linearly across the record set — fine for the size of data that belongs in
135
+ `DataFor` in the first place.
136
+
137
+ ### Custom primary keys
138
+
139
+ The default primary key is `:id`. Override it with `self.primary_key=` when
140
+ your data uses a different identifier:
141
+
142
+ ```ruby
143
+ Book = Data.define(:isbn, :title, :author) do
144
+ include DataFor::Model
145
+ config :books
146
+ self.primary_key = :isbn
147
+ end
148
+
149
+ Book.find("978-0-13-468599-1")
150
+ ```
151
+
152
+ ### Casting attributes with `cast_<member>`
153
+
154
+ For every member of your `Data` class, `DataFor::Model` generates a private
155
+ `cast_<member>` method that runs at construction time. The default
156
+ implementation is the identity function. Override the cast method to coerce,
157
+ parse, or nest:
158
+
159
+ ```ruby
160
+ Country = Data.define(:id, :name, :states) do
161
+ include DataFor::Model
162
+ config :countries
163
+
164
+ private
165
+
166
+ def cast_states(data)
167
+ Array(data).map { State[**it] }
168
+ end
169
+ end
170
+ ```
171
+
172
+ Now every `Country#states` returns an array of `State` value objects rather
173
+ than raw hashes:
174
+
175
+ ```ruby
176
+ Country.find("US").states.first
177
+ # => #<data State id="AL", name="Alabama", country_id="US">
178
+ ```
179
+
180
+ ### Reprojecting one config to drive multiple models
181
+
182
+ `config` accepts a `project:` proc that transforms the loaded data before it
183
+ becomes the model's record set. This lets a single YAML file power multiple
184
+ query surfaces:
185
+
186
+ ```ruby
187
+ State = Data.define(:id, :name, :country_id) do
188
+ include DataFor::Model
189
+ config :countries, project: -> { it.pluck(:states).flatten }
190
+ end
191
+
192
+ State.where(country_id: "US")
193
+ # => [#<data State id="AL", ...>, #<data State id="AK", ...>, ...]
194
+
195
+ Country.find("US").states == State.where(country_id: "US")
196
+ # => true
197
+ ```
198
+
199
+ ### Loading data outside of Rails
200
+
201
+ By default, `config` reads data via `Rails.application.config_for(filename)`.
202
+ Pass a `loader:` proc to load data from somewhere else:
203
+
204
+ ```ruby
205
+ Plan = Data.define(:id, :name, :price_cents) do
206
+ include DataFor::Model
207
+ config :plans, loader: ->(name) { YAML.load_file("data/#{name}.yml") }
208
+ end
209
+ ```
210
+
211
+ ## Caching & reloading
212
+
213
+ `config` reads the file once, applies the `project:` proc, freezes the
214
+ result, and caches it in a class instance variable. Subsequent queries hit
215
+ in-memory data structures — no file I/O, no parsing.
216
+
217
+ - **In `production`** (with `config.cache_classes = true`), models load at
218
+ boot and stay until the process restarts.
219
+ - **In `development`**, Rails reloads model classes on every request, which
220
+ re-evaluates the `Data.define` block and re-runs `config`. Edits to your
221
+ YAML files take effect on the next request — no server restart.
222
+ - **In `test`**, behavior matches `production` by default.
223
+
224
+ If you call `config` a second time on the same class (rare), the internal
225
+ `@all` and `@index` memos are cleared so the new data takes over cleanly.
226
+
227
+ ## Alternatives
228
+
229
+ `DataFor` is not the first gem in this space. The closest neighbors:
230
+
231
+ ### [`data_for`](https://github.com/OutlawAndy/data_for) *(this gem)*
232
+
233
+ - **Storage:** Rails `config_for` (YAML)
234
+ - **Record type:** Ruby `Data.define` (immutable value object)
235
+ - **Distinct features:** idiomatic Rails configuration, immutable records, and
236
+ the `project:` proc reshapes one config file into multiple query surfaces.
237
+
238
+ ### [`frozen_record`](https://github.com/byroot/frozen_record)
239
+
240
+ - **Storage:** YAML / JSON / custom backends
241
+ - **Record type:** ActiveRecord-style class
242
+ - **Distinct features:** the most mature gem in this space, broadest query API,
243
+ pluggable backends. Records are AR-style classes, not Ruby `Data`.
244
+
245
+ ### [`active_hash`](https://github.com/active-hash/active_hash)
246
+
247
+ - **Storage:** in-memory hashes (`active_hash`), YAML (`active_yaml`), JSON, ENUM
248
+ - **Record type:** ActiveRecord-style class
249
+ - **Distinct features:** AR-compatible API, supports associations to AR models.
250
+ Long-established.
251
+
252
+ ### [`yaml_record`](https://github.com/joshbuddy/yaml_record)
253
+
254
+ - **Storage:** YAML
255
+ - **Record type:** custom class
256
+ - **Status:** older, less active.
257
+
258
+ If you need AR-style associations across both real DB tables and your
259
+ reference data, `active_hash` is the best fit. If you want the broadest
260
+ query API and don't care about value-object semantics, `frozen_record` is
261
+ the most mature option. `DataFor` is the smallest gem of the bunch and the
262
+ only one built on native Ruby `Data` objects with Rails-idiomatic
263
+ configuration.
264
+
265
+ ## Contributing
266
+
267
+ Bug reports and pull requests are welcome on GitHub at
268
+ <https://github.com/OutlawAndy/data_for>.
269
+
270
+ ## License
271
+
272
+ `DataFor` is available as open source under the terms of the
273
+ [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,3 @@
1
+ module DataFor
2
+ VERSION = "0.1.0"
3
+ end
data/lib/data_for.rb ADDED
@@ -0,0 +1,75 @@
1
+ require "active_support/concern"
2
+ require "active_support/core_ext/enumerable"
3
+ require "data_for/version"
4
+
5
+ module DataFor
6
+ class RecordNotFound < StandardError; end
7
+
8
+ module Model
9
+ extend ActiveSupport::Concern
10
+
11
+ DEFAULT_LOADER = ->(filename) { Rails.application.config_for(filename) }
12
+
13
+ included do
14
+ class << self
15
+ attr_accessor :source_data
16
+ attr_writer :primary_key
17
+
18
+ def primary_key
19
+ @primary_key ||= :id
20
+ end
21
+ end
22
+
23
+ members.each do |member|
24
+ private(define_method(:"cast_#{member}") { it })
25
+ end
26
+ end
27
+
28
+ def initialize(**kwargs)
29
+ super(**members.index_with { send(:"cast_#{it}", kwargs[it]) })
30
+ end
31
+
32
+ class_methods do
33
+ def config(filename, project: -> { it }, loader: DEFAULT_LOADER)
34
+ self.source_data = project[loader[filename]].freeze
35
+ @all = nil
36
+ @index = nil
37
+ source_data
38
+ end
39
+
40
+ def all
41
+ @all ||= source_data.map { new(**it) }
42
+ end
43
+
44
+ def find(value)
45
+ index[value]
46
+ end
47
+
48
+ def find!(value)
49
+ find(value) or raise RecordNotFound,
50
+ "Couldn't find #{name} with #{primary_key}=#{value.inspect}"
51
+ end
52
+
53
+ def find_by(data)
54
+ all.find { match_all?(data, it) }
55
+ end
56
+
57
+ def find_by!(data)
58
+ find_by(data) or raise RecordNotFound,
59
+ "Couldn't find #{name} matching #{data.inspect}"
60
+ end
61
+
62
+ def where(data)
63
+ all.select { match_all?(data, it) }
64
+ end
65
+
66
+ def index
67
+ @index ||= all.index_by { it.public_send(primary_key) }
68
+ end
69
+
70
+ def match_all?(constraints, resource)
71
+ constraints.all? { |key, value| resource.public_send(key) == value }
72
+ end
73
+ end
74
+ end
75
+ end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: data_for
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Andy Cohen
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: railties
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '6.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '6.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: activesupport
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '6.1'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '6.1'
40
+ description: DataFor turns YAML in your Rails config directory into queryable, read-only
41
+ models -- countries, currencies, plans, and other reference data that rarely changes
42
+ and doesn't belong in your database. Built on Ruby's Data.define and Rails' config_for,
43
+ with a familiar find, find_by, and where API, plus a transform proc that lets one
44
+ config file power multiple query surfaces.
45
+ email:
46
+ - outlawandy@gmail.com
47
+ executables: []
48
+ extensions: []
49
+ extra_rdoc_files: []
50
+ files:
51
+ - CHANGELOG.md
52
+ - MIT-LICENSE
53
+ - README.md
54
+ - lib/data_for.rb
55
+ - lib/data_for/version.rb
56
+ homepage: https://github.com/OutlawAndy/data_for
57
+ licenses:
58
+ - MIT
59
+ metadata:
60
+ homepage_uri: https://github.com/OutlawAndy/data_for
61
+ source_code_uri: https://github.com/OutlawAndy/data_for
62
+ changelog_uri: https://github.com/OutlawAndy/data_for/blob/main/CHANGELOG.md
63
+ rubygems_mfa_required: 'true'
64
+ rdoc_options: []
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: '3.4'
72
+ required_rubygems_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ requirements: []
78
+ rubygems_version: 4.0.10
79
+ specification_version: 4
80
+ summary: Queryable, read-only Ruby Data models backed by Rails config files.
81
+ test_files: []