├── .github
└── workflows
│ └── ruby.yml
├── .gitignore
├── CHANGELOG.md
├── Gemfile
├── LICENSE
├── README.md
├── Rakefile
├── active_hash.gemspec
├── gemfiles
├── rails_6.1.gemfile
├── rails_7.0.gemfile
├── rails_7.1.gemfile
├── rails_7.2.gemfile
└── rails_8.0.gemfile
├── lib
├── active_file
│ ├── base.rb
│ ├── hash_and_array_files.rb
│ └── multiple_files.rb
├── active_hash.rb
├── active_hash
│ ├── base.rb
│ ├── condition.rb
│ ├── conditions.rb
│ ├── relation.rb
│ └── version.rb
├── active_json
│ └── base.rb
├── active_yaml
│ ├── aliases.rb
│ └── base.rb
├── associations
│ ├── associations.rb
│ └── reflection_extensions.rb
└── enum
│ └── enum.rb
└── spec
├── active_file
├── base_spec.rb
└── multiple_files_spec.rb
├── active_hash
├── base_spec.rb
└── relation_spec.rb
├── active_json
└── base_spec.rb
├── active_yaml
├── aliases_spec.rb
└── base_spec.rb
├── associations
├── active_record_extensions_spec.rb
└── associations_spec.rb
├── enum
└── enum_spec.rb
├── fixtures
├── array_products.yml
├── array_products_2.yml
├── array_rows.json
├── array_rows.yml
├── boroughs.yml
├── cities.json
├── cities.yml
├── commonwealths.json
├── commonwealths.yml
├── countries.json
├── countries.yml
├── empties.json
├── empties.yml
├── key_products.yml
├── locales
│ └── ja.yml
├── provinces.json
├── provinces.yml
├── states.json
├── states.yml
└── users.yml
└── spec_helper.rb
/.github/workflows/ruby.yml:
--------------------------------------------------------------------------------
1 | name: Ruby
2 | concurrency:
3 | group: "${{github.workflow}}-${{github.ref}}"
4 | cancel-in-progress: true
5 | on:
6 | workflow_dispatch:
7 | schedule:
8 | - cron: "0 8 * * 3" # At 08:00 on Wednesday # https://crontab.guru/#0_8_*_*_3
9 | push:
10 | branches:
11 | - master
12 | tags:
13 | - v*.*.*
14 | pull_request:
15 | types: [opened, synchronize]
16 | branches:
17 | - '*'
18 |
19 | jobs:
20 | build:
21 | runs-on: ubuntu-latest
22 | strategy:
23 | fail-fast: false
24 | matrix:
25 | # | rails | rails EOL | minruby | maxruby |
26 | # |-------+-----------+---------+---------+
27 | # | 6.1 | 10/2024 | 2.5 | 3.3 |
28 | # | 7.0 | 4/2025 | 2.7 | 3.3 |
29 | # | 7.1 | 10/2025 | 2.7 | |
30 | # | 7.2 | 8/2026 | 3.1 | |
31 | # | 8.0 | ~11/2026 | 3.2 | |
32 | ruby: ['3.0', '3.1', '3.2', '3.3', '3.4']
33 | gemfile:
34 | - 'gemfiles/rails_6.1.gemfile'
35 | - 'gemfiles/rails_7.0.gemfile'
36 | - 'gemfiles/rails_7.1.gemfile'
37 | - 'gemfiles/rails_7.2.gemfile'
38 | - 'gemfiles/rails_8.0.gemfile'
39 | exclude:
40 | - ruby: '3.0'
41 | gemfile: 'gemfiles/rails_7.2.gemfile'
42 | - ruby: '3.0'
43 | gemfile: 'gemfiles/rails_8.0.gemfile'
44 | - ruby: '3.1'
45 | gemfile: 'gemfiles/rails_8.0.gemfile'
46 | - ruby: '3.4'
47 | gemfile: 'gemfiles/rails_6.1.gemfile'
48 | - ruby: '3.4'
49 | gemfile: 'gemfiles/rails_7.0.gemfile'
50 | env:
51 | BUNDLE_GEMFILE: ${{ matrix.gemfile }}
52 | name: Ruby ${{ matrix.ruby }}, ${{ matrix.gemfile }}
53 | steps:
54 | - uses: actions/checkout@v4
55 | - name: Set up Ruby
56 | uses: ruby/setup-ruby@v1
57 | with:
58 | ruby-version: ${{ matrix.ruby }}
59 | bundler-cache: true
60 | - name: Run tests
61 | run: bundle exec rake spec
62 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.gem
2 | *.sw?
3 | .DS_Store
4 | .bundle/*
5 | .idea
6 | .rvmrc
7 | Gemfile.lock
8 | coverage
9 | junk.*
10 | pkg
11 | rdoc
12 | vendor/bundle
13 | gemfiles/.bundle
14 | gemfiles/vendor
15 | gemfiles/*.lock
16 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # active_hash Changelog
2 |
3 | ## Version [3.3.1] - 2024-05-03
4 |
5 | ### Fixed
6 |
7 | - Fix `hash_many :through` associations which specify a scope. [#306](https://github.com/active-hash/active_hash/pull/306) @sontixyou
8 |
9 |
10 | ## Version [3.3.0] - 2024-04-30
11 |
12 | ### Added
13 |
14 | - Ruby 3.3 support [#298](https://github.com/active-hash/active_hash/pull/298) @m-nakamura145
15 | - Support `has_many :through` associations [#296](https://github.com/active-hash/active_hash/pull/296) @flavorjones
16 | - Rails 7.1 support [#291](https://github.com/active-hash/active_hash/pull/291) @y-yagi
17 |
18 | ### Fixed
19 |
20 | - Rails 7.1: fix sqlite3 issue [#303](https://github.com/active-hash/active_hash/pull/303) @flavorjones
21 | - Rails 7.1.3: add missing `has_query_constraints?` [#300](https://github.com/active-hash/active_hash/pull/300) @flavorjones
22 | - `Array#pluck` supports methods [#299](https://github.com/active-hash/active_hash/pull/299) @iberianpig
23 | - Prefer `safe_constantize` over `constantize` [#297](https://github.com/active-hash/active_hash/pull/297) @flavorjones
24 | - Treat `nil` and `blank?` as different values [#295](https://github.com/active-hash/active_hash/pull/295) @kbrock
25 | - Fix `#where` for string keys [#292](https://github.com/active-hash/active_hash/pull/292) @usernam3
26 |
27 | ## Version [3.2.1] - 2023-08-31
28 |
29 | ### Added
30 |
31 | - Improve `pp` output for `ActiveHash::Relation`. [#288](https://github.com/active-hash/active_hash/pull/288) @flavorjones
32 |
33 | ### Fixed
34 |
35 | - Fix relation matching when attribute name collides with a method. [#281](https://github.com/active-hash/active_hash/pull/281) @flavorjones
36 | - Fix association reflection in applications that don't use ActiveHash::Associations. [#286](https://github.com/active-hash/active_hash/pull/286) @iberianpig
37 | - Fix `ActiveHash::Relation#method_missing` and `#respond_to_missing?` without scopes. [#278](https://github.com/active-hash/active_hash/pull/278) @julianrubisch
38 |
39 |
40 | ## Version [3.2.0] - 2023-05-06
41 |
42 | - Add Ruby 3.2 to the CI matrix [#275](https://github.com/active-hash/active_hash/pull/275) @petergoldstein
43 | - Handle default value of `false` [#274](https://github.com/active-hash/active_hash/pull/274) @ihollander
44 | - Run CI only one time per commit [#273](https://github.com/active-hash/active_hash/pull/273) @flavorjones
45 | - Rails 7 support [#272](https://github.com/active-hash/active_hash/pull/272) @flavorjones
46 | - Avoid interfere with AR's belongs_to arguments. [#270](https://github.com/active-hash/active_hash/pull/270) @koyo-miyamura
47 | - Fix broken #pluck method with 3+ attrs specified [#269](https://github.com/active-hash/active_hash/pull/269) @h6ah4i
48 | - Fix relations for Rails 7 support, and not modifying conditions [#268](https://github.com/active-hash/active_hash/pull/268) @pfeiffer
49 | - docs: Remove the string 'F' [#264](https://github.com/active-hash/active_hash/pull/264) @tbotaq
50 | - Show example using regex in where query [#263](https://github.com/active-hash/active_hash/pull/263) @scottharvey
51 | - Improve performance of exists? [#262](https://github.com/active-hash/active_hash/pull/262) @ise-tang
52 | - Remove redundant ActiveRecord version check [#260](https://github.com/active-hash/active_hash/pull/260) @yujideveloper
53 | - Fix deprecation warnings [#259](https://github.com/active-hash/active_hash/pull/259) @yujideveloper
54 | - Fix rspec config when SKIP_ACTIVE_RECORD enabled [#258](https://github.com/active-hash/active_hash/pull/258) @yujideveloper
55 | - isolate tests with temporary classes [#256](https://github.com/active-hash/active_hash/pull/256) @machisuke
56 | - Avoid ActiveRecordExtensions affects AR's belongs_to method. [#255](https://github.com/active-hash/active_hash/pull/255) @machisuke
57 | - add option to disable erb parsing [#202](https://github.com/active-hash/active_hash/pull/202) @reedlaw
58 | - add collection singular ids for associations [#237](https://github.com/active-hash/active_hash/pull/237) @1160054
59 | - Fix the thread-safe spec for the updated cities fixture @adampal
60 | - Add thread-safety to ActiveFile [#229](https://github.com/active-hash/active_hash/pull/229) @dmitriy-kiriyenko
61 |
62 | ## Version [3.1.1] - 2022-07-14
63 |
64 | - Make scopes chainable [#248](https://github.com/active-hash/active_hash/pull/248) @andreynering
65 | - Set default key attributes [#251](https://github.com/active-hash/active_hash/pull/251/commits/68a0a121d110ac83f4bbf0024f027714fd24debf) @adampal
66 | - Migrate from Travis to GitHub Actions for CI @kbrock
67 | - Add primary_key support for has_one [#218](https://github.com/active-hash/active_hash/pull/218) @yujideveloper
68 | - Return a chainable relation when using .not [#205](https://github.com/active-hash/active_hash/pull/205) @pfeiffer
69 | - Correct fields with YAML aliases in array style [#226](https://github.com/active-hash/active_hash/pull/226) @stomk
70 | - Add ActiveHash::Relation#size method for compatibily [#227](https://github.com/active-hash/active_hash/pull/227) @sluceno
71 | - Implement ActiveRecord::RecordNotFound interface [#207](https://github.com/active-hash/active_hash/pull/207) @ChrisBr
72 | - Fix find_by_id with filter chain [#210](https://github.com/active-hash/active_hash/pull/210) @ChrisBr
73 | - Suppress Ruby 2.7 kwargs warnings [#206](https://github.com/active-hash/active_hash/pull/206) @yhirano55
74 | - Call reload if @records is not defined [#208](https://github.com/active-hash/active_hash/pull/208) @jonmagic
75 | - Switch to rspec3 (and update the Gemfile) [#209](https://github.com/active-hash/active_hash/pull/209) @djberg96
76 | - Implement filter by RegEx [#211](https://github.com/active-hash/active_hash/pull/211) @ChrisBr
77 | - Supports .pick method [#195](https://github.com/active-hash/active_hash/pull/195/files) @yhirano55
78 | - Lots of other small performance improvements, documentation and testing. Thanks to everyone who contributed!
79 |
80 | ## Version [3.1.0] - 2020-01-15
81 |
82 | - Add ActiveHash::Base.order method inspired by ActiveRecord [#177](https://github.com/active-hash/active_hash/pull/177)
83 | - Add #to_ary to ActiveHash::Relation [#182](https://github.com/active-hash/active_hash/pull/182)
84 | - Allow #find to behave like Enumerable#find if id is nil and a block is given [#183](https://github.com/active-hash/active_hash/pull/183)
85 | - Delegate :sample to `records` [#189](https://github.com/active-hash/active_hash/pull/189)
86 |
87 | ## Version [3.0.0] - 2019-09-28
88 |
89 | - Make #where chainable [#178](https://github.com/active-hash/active_hash/pull/178)
90 |
91 | ## Version [2.3.0] - 2019-09-28
92 |
93 | - Add ::scope method (inspired by ActiveRecord) [#173](https://github.com/active-hash/active_hash/pull/173)
94 | - Let `.find(nil)` raise ActiveHash::RecordNotFound (inspired by ActiveRecord) [#174](https://github.com/active-hash/active_hash/pull/174)
95 | - `where` clause now works with range argument [#175](https://github.com/active-hash/active_hash/pull/175)
96 |
97 | ## Version [2.2.1] - 2019-03-06
98 |
99 | - Allow empty YAML [#171](https://github.com/active-hash/active_hash/pull/171) Thanks, @ppworks
100 |
101 | ## Version [2.2.0] - 2018-11-22
102 |
103 | - Support pluck method [#164](https://github.com/active-hash/active_hash/pull/164) Thanks, @ihatov08
104 | - Support where.not method [#167](https://github.com/active-hash/active_hash/pull/167) Thanks, @DialBird
105 |
106 | ## Version [2.1.0] - 2018-04-05
107 |
108 | - Allow to use ERB (embedded ruby) in yml files [#160](https://github.com/active-hash/active_hash/pull/160) Thanks, @UgoMare
109 | - Add `ActiveHash::Base.polymorphic_name` [#162](https://github.com/active-hash/active_hash/pull/162)
110 | - Fix to be able to use enum accessor constant with same name as top-level constant[#161](https://github.com/active-hash/active_hash/pull/161) Thanks, @yujideveloper
111 |
112 | ## Version [2.0.0] - 2018-02-27
113 |
114 | - Drop old Ruby and Rails support [#157](https://github.com/active-hash/active_hash/pull/157)
115 | - Don't generate instance accessors for class attributes [#136](https://github.com/active-hash/active_hash/pull/136) Thanks, @rainhead
116 |
117 | ## Version [1.5.3] - 2017-06-14
118 |
119 | - Support symbol values in where and find_by [#156](https://github.com/active-hash/active_hash/pull/156) Thanks, @south37
120 |
121 | ## Version [1.5.2] - 2017-06-14
122 |
123 | - Fix find_by when passed an invalid id [#152](https://github.com/active-hash/active_hash/pull/152) Thanks, @davidstosik
124 |
125 | ## Version [1.5.1] - 2017-04-20
126 |
127 | - Fix a bug on `.where` [#147](https://github.com/active-hash/active_hash/pull/147)
128 |
129 | ## Version [1.5.0] - 2017-03-24
130 |
131 | - add support for `.find_by!`(@syguer)
132 |
133 | ## Version [1.4.1] - 2015-09-13
134 |
135 | - fix bug where `#attributes` didn't contain default values [#107](https://github.com/active-hash/active_hash/pull/107)
136 | - add support for `.find_by` and `#_read_attribute`. Thanks, @andrewfader
137 |
138 | ## Version [1.4.0] - 2014-09-03
139 |
140 | - support Rails 4.2 @agraves, @al2o3cr
141 |
142 | ## Version [1.3.0] - 2014-02-18
143 |
144 | - fix bug where including ActiveHash associations would make `belongs_to :imageable, polymorphic: true` blow up
145 | - fixed several bugs that prevented active hash from being used without active record / active model
146 | - add support for splitting up data sources into multiple files @rheaton
147 | - add support for storing data in json files @rheaton
148 |
149 | ## Version [1.2.3] - 2013-11-29
150 |
151 | - fix bug where active hash would call `.all` on models when setting has_many @grosser
152 |
153 | ## Version [1.2.2] - 2013-11-05
154 |
155 | - fix bug in gemspec that made it impossible to use w/ Rails 4
156 |
157 | ## Version [1.2.1] - 2013-10-24
158 |
159 | - fixed nasty bug in belongs_to that would prevent users from passing procs @freebird0221
160 | - fixed bug where passing in a separate class name to belongs_to_active_hash would raise an exception @mauriciopasquier
161 |
162 | ## Version [1.2.0] - 2013-10-01
163 |
164 | - belongs_to is back!
165 | - added support for primary key options for belongs_to @tomtaylor
166 |
167 | ## Version [1.0.2] - 2013-09-09
168 |
169 | - `where(nil)` returns all results, like ActiveRecord @kugaevsky
170 |
171 | ## Version [1.0.1] - 2013-07-15
172 |
173 | - Travis CI for ActiveHash + Ruby 2, 1.8.7, Rubinius and JRuby support @mattheworiordan
174 | - no longer need to call .all before executing `find_by_*` or `where` methods @mattheworiordan
175 |
176 | ## Version [1.0.0] - 2013-06-24
177 |
178 | - save is a no-op on existing records, instead of raising an error (issue #63)
179 |
180 | ## Version [0.10.0] - 2013-06-24
181 |
182 | - added ActiveYaml::Aliases module so you can DRY up your repetitive yaml @brett-richardson
183 |
184 | ## Version [0.9.14] - 2013-05-23
185 |
186 | - enum_accessor can now take multiple field names when generating the constant
187 | - temporarily disabled rails edge specs since there's an appraisal issue with minitest
188 |
189 | ## Version [0.9.13] 2013-01-22
190 | - Fix find_by_id and find method returning nil unless .all called in ActiveYaml @mattheworiordan
191 |
192 | ## Version [0.9.12] 2012-07-25
193 | - Make find_by_id lookups faster by indexing records by id @desmondmonster
194 |
195 | ## Version [0.9.11] 2012-07-16
196 | - Validate IDs are unique by caching them in a set @desmondmonster
197 |
198 | ## Version [0.9.10] 2012-04-14
199 | - Support for has_one associations @kbrock
200 |
201 | ## Version [0.9.9] 2012-04-05
202 |
203 | - Allow gems like simple_form to read metadata about belongs_to associations that point to active hash objects @kbrock
204 | - Move specs to appraisal @flavorjones
205 |
206 | ## Version [0.9.8] - 2012-01-18
207 |
208 | - Make ActiveHash.find with array raise an exception when record cannot be found @mocoso
209 |
210 | ## Version [0.9.7] - 2011-09-18
211 |
212 | - Fixing the setting of a `belongs_to_active_hash` association by association (not id).
213 |
214 | ## Version [0.9.6] - 2011-08-31
215 | - added a module which adds a .belongs_to_active_hash method to ActiveRecord, since it was broken for Rails 3.1 @felixclack
216 |
217 | ## Version [0.9.5] - 2011-06-07
218 | - fixed bug where .find would not work if you defined your ids as strings
219 |
220 | ## Version [0.9.4] - 2011-06-05
221 | - fixed deprecation warnings for class_inheritable_accessor @scudco
222 | - added basic compatibility with the `where` method from Arel @rgarver
223 |
224 | ## Version [0.9.3] - 2011-04-19
225 | - better dependency management and compatibility with ActiveSupport 2.x @vandrijevik
226 |
227 | ## Version [0.9.2] - 2011-01-22
228 | - improved method_missing errors for dynamic finders
229 | - prevent users from trying to overwrite :attributes [#33](https://github.com/active-hash/active_hash/issues/33)
230 |
231 | ## Version [0.9.1] 2010-12-08
232 | - ruby 1.9.2 compatibility
233 |
234 | ## Version [0.9.0] 2010-12-06
235 | - added dependency on ActiveModel
236 | - add persisted? method to ActiveHash::Base
237 | - ActiveHash::Base#save takes \*args to be compatible with ActiveModel
238 | - ActiveHash::Base#to_param returns nil if the object hasn't been saved
239 |
240 | ## Version [0.8.7] 2010-11-09
241 | - Use Ruby's definition of "word character" (numbers, underscores) when forming ActiveHash::Enum constants @tstuart
242 |
243 | ## Version [0.8.6] 2010-11-07
244 | - Get ActiveHash::Associations to return a scope for has_many active record relationships @mocoso
245 |
246 | ## Version [0.8.5] 2010-10-20
247 | - Allow find_by_* methods to accept an options hash, so rails associations don't blow up
248 |
249 | ## Version [0.8.4] 2010-10-07
250 | - Add conditions to ActiveHash#all (Ryan Garver)
251 | - Add #cache_key to ActiveHash::Base (Tom Stuart)
252 | - Add banged dynamic finder support to ActiveHash::Base (Tom Stuart)
253 |
254 | ## Version [0.8.3] 2010-09-16
255 | - Enum format now uses underscores instead of removing all characters
256 | - Removed test dependency on acts_as_fu
257 |
258 | ## Version [0.8.2] 2010-05-26
259 | - Silence metaclass deprecation warnings in active support 2.3.8
260 |
261 | ## Version [0.8.1] 2010-05-04
262 | - When calling ActiveFile::Base.reload do not actually perform the reload if nothing has been modified unless you call reload(true) to force (Michael Schubert)
263 |
264 | ## Version [0.8.0] 2010-04-25
265 | - When ActiveRecord model belongs_to an ActiveHash and the associated id is nil, returns nil instead of raising RecordNotFound (Jeremy Weiskotten)
266 | - Merged Nakajima's "add" alias for "create" - gotta save those ASCII characters :)
267 |
268 | ## Version [0.7.9] 2010-03-01
269 | - Removed "extend"-related deprecations - they didn't play well with rails class loading
270 |
271 | ## Version [0.7.8] 2010-01-18
272 | - Added stub for #destroyed? method, since Rails associations now depend on it
273 |
274 | ## Version [0.7.7] 2009-12-19
275 | - Deprecated include ActiveHash::Associations in favor of extend ActiveHash::Associations
276 |
277 | ## Version [0.7.6] 2009-12-19
278 | - Added ActiveHash::Enum (John Pignata)
279 | - Fixed bug where you can't set nil to an association
280 | - Calling #belongs_to now creates the underlying field if it's not already there (belongs_to :city will create the :city_id field)
281 |
282 | ## Version [0.7.5] 2009-12-10
283 | - Fixed a bug where belongs_to associations would raise an error instead of returning nil when the parent object didn't exist.
284 | - Added #[] and #[]= accessors for more ActiveRecord-esque-ness. (Pat Nakajima & Dave Yeu)
285 |
286 | ## Version [0.7.4] 2009-12-01
287 | - Add marked_for_destruction? to be compatible with nested attributes (Brandon Keene)
288 | - Added second parameter to respond_to? and cleaned up specs (Brian Takita)
289 | - Find with an id that does not exist now raises a RecordNotFound exception to mimic ActiveRecord (Pat Nakajima)
290 |
291 | ## Version [0.7.3] 2009-10-22
292 | - added setters to ActiveHash::Base for all fields
293 | - instantiating an ActiveHash object with a hash calls the setter methods on the object
294 | - boolean default values now work
295 |
296 | ## Version [0.7.2] 2009-10-21
297 | - Removed auto-reloading of files based on mtime - maybe it will come back later
298 | - Made ActiveFile::Base.all a bit more sane
299 |
300 | ## Version 0.7.1 2009-10-13
301 | - added ActiveHash::Base.has_many, which works with ActiveRecord or ActiveHash classes @baldwindavid
302 | - added ActiveHash::Base.belongs_to, which works with ActiveRecord or ActiveHash classes @baldwindavid
303 | - added .delete_all method that clears the in-memory array
304 | - added support for Hash-style yaml (think, Rails fixtures)
305 | - added setter for parent object on belongs_to ( `city = City.new; city.state = State.first; city.state_id == State.first.id` )
306 |
307 | ## Version [0.7.0] 2009-10-12
308 | - auto-assign fields after calling data= instead of after calling .all
309 | - remove require 'rubygems', so folks with non-gem setups can still use AH
310 | - added more specific activesupport dependency to ensure that metaclass is available
311 | - AH no longer calls to_i on ids. If you pass in a string as an id, you'll get a string back
312 | - Fancy finders, such as find_all_by_id_and_name, will compare the to_s values of the fields, so you can pass in strings
313 | - You can now use ActiveHash models as the parents of polymorphic belongs_to associations
314 | - save, save!, create and create! now add items to the in-memory collection, and naively adds autoincrementing id
315 | - new_record? returns false if the record is part of the collection
316 | - ActiveHash now works with Fixjour!
317 |
318 | ## Version [0.6.1] 2009-08-19
319 | - Added custom finders for multiple fields, such as .find_all_by_name_and_age
320 |
321 | ## Version 0.5.0 2009-07-23
322 | - Added support for auto-defining methods based on hash keys in ActiveHash::Base
323 | - Changed the :field and :fields API so that they don't overwrite existing methods (useful when ActiveHash auto-defines methods)
324 | - Fixed a bug where ActiveFile incorrectly set the root_path to be the path in the gem directory, not the current working directory
325 |
326 | ## Version 0.4.0 2009-07-24
327 | - ActiveFile no longer reloads files by default
328 | - Added ActiveFile.reload_active_file= so you can cause ActiveFile to reload
329 | - Setting data to nil correctly causes .all to return an empty array
330 | - Added reload(force) method, so that you can force a reload from files in ActiveFile, useful for tests
331 |
332 | [HEAD]: https://github.com/active-hash/active_hash/compare/v3.3.0...HEAD
333 | [3.3.0]: https://github.com/active-hash/active_hash/compare/v3.2.1...v3.3.0
334 | [3.2.1]: https://github.com/active-hash/active_hash/compare/v3.2.0...v3.2.1
335 | [3.2.0]: https://github.com/active-hash/active_hash/compare/v3.1.1...v3.2.0
336 | [3.1.1]: https://github.com/active-hash/active_hash/compare/v3.1.0...v3.1.1
337 | [3.1.0]: https://github.com/active-hash/active_hash/compare/v3.0.0...v3.1.0
338 | [3.0.0]: https://github.com/active-hash/active_hash/compare/v2.3.0...v3.0.0
339 | [2.3.0]: https://github.com/active-hash/active_hash/compare/v2.2.1...v2.3.0
340 | [2.2.1]: https://github.com/active-hash/active_hash/compare/v2.2.0...v2.2.1
341 | [2.2.0]: https://github.com/active-hash/active_hash/compare/v2.1.0...v2.2.0
342 | [2.1.0]: https://github.com/active-hash/active_hash/compare/v2.0.0...v2.1.0
343 | [2.0.0]: https://github.com/active-hash/active_hash/compare/v1.5.3...v2.0.0
344 | [1.5.3]: https://github.com/active-hash/active_hash/compare/v1.5.2...v1.5.3
345 | [1.5.2]: https://github.com/active-hash/active_hash/compare/v1.5.1...v1.5.2
346 | [1.5.1]: https://github.com/active-hash/active_hash/compare/v1.5.0...v1.5.1
347 | [1.5.0]: https://github.com/active-hash/active_hash/compare/v1.4.1...v1.5.0
348 | [1.4.1]: https://github.com/active-hash/active_hash/compare/v1.4.0...v1.4.1
349 | [1.4.0]: https://github.com/active-hash/active_hash/compare/v1.3.0...v1.4.0
350 | [1.3.0]: https://github.com/active-hash/active_hash/compare/v1.2.3...v1.3.0
351 | [1.2.3]: https://github.com/active-hash/active_hash/compare/v1.2.2...v1.2.3
352 | [1.2.2]: https://github.com/active-hash/active_hash/compare/v1.2.1...v1.2.2
353 | [1.2.1]: https://github.com/active-hash/active_hash/compare/v1.2.0...v1.2.1
354 | [1.2.0]: https://github.com/active-hash/active_hash/compare/v1.0.2...v1.2.0
355 | [1.0.2]: https://github.com/active-hash/active_hash/compare/v1.0.1...v1.0.2
356 | [1.0.1]: https://github.com/active-hash/active_hash/compare/v1.0.0...v1.0.1
357 | [1.0.0]: https://github.com/active-hash/active_hash/compare/v0.10.0...v1.0.0
358 | [0.10.0]: https://github.com/active-hash/active_hash/compare/v0.9.14...v0.10.0
359 | [0.9.14]: https://github.com/active-hash/active_hash/compare/v0.9.13...v0.9.14
360 | [0.9.13]: https://github.com/active-hash/active_hash/compare/v0.9.12...v0.9.13
361 | [0.9.12]: https://github.com/active-hash/active_hash/compare/v0.9.11...v0.9.12
362 | [0.9.11]: https://github.com/active-hash/active_hash/compare/v0.9.10...v0.9.11
363 | [0.9.10]: https://github.com/active-hash/active_hash/compare/v0.9.9...v0.9.10
364 | [0.9.9]: https://github.com/active-hash/active_hash/compare/v0.9.8...v0.9.9
365 | [0.9.8]: https://github.com/active-hash/active_hash/compare/v0.9.7...v0.9.8
366 | [0.9.7]: https://github.com/active-hash/active_hash/compare/v0.9.6...v0.9.7
367 | [0.9.6]: https://github.com/active-hash/active_hash/compare/v0.9.5...v0.9.6
368 | [0.9.5]: https://github.com/active-hash/active_hash/compare/v0.9.4...v0.9.5
369 | [0.9.4]: https://github.com/active-hash/active_hash/compare/v0.9.3...v0.9.4
370 | [0.9.3]: https://github.com/active-hash/active_hash/compare/v0.9.2...v0.9.3
371 | [0.9.2]: https://github.com/active-hash/active_hash/compare/v0.9.1...v0.9.2
372 | [0.9.1]: https://github.com/active-hash/active_hash/compare/v0.9.0...v0.9.1
373 | [0.9.0]: https://github.com/active-hash/active_hash/compare/v0.8.7...v0.9.0
374 | [0.8.7]: https://github.com/active-hash/active_hash/compare/v0.8.6...v0.8.7
375 | [0.8.6]: https://github.com/active-hash/active_hash/compare/v0.8.5...v0.8.6
376 | [0.8.5]: https://github.com/active-hash/active_hash/compare/v0.8.4...v0.8.5
377 | [0.8.4]: https://github.com/active-hash/active_hash/compare/v0.8.3...v0.8.4
378 | [0.8.3]: https://github.com/active-hash/active_hash/compare/v0.8.2...v0.8.3
379 | [0.8.2]: https://github.com/active-hash/active_hash/compare/v0.8.1...v0.8.2
380 | [0.8.1]: https://github.com/active-hash/active_hash/compare/v0.8.0...v0.8.1
381 | [0.8.0]: https://github.com/active-hash/active_hash/compare/v0.7.9...v0.8.0
382 | [0.7.9]: https://github.com/active-hash/active_hash/compare/v0.7.8...v0.7.9
383 | [0.7.8]: https://github.com/active-hash/active_hash/compare/v0.7.7...v0.7.8
384 | [0.7.7]: https://github.com/active-hash/active_hash/compare/v0.7.6...v0.7.7
385 | [0.7.6]: https://github.com/active-hash/active_hash/compare/v0.7.5...v0.7.6
386 | [0.7.5]: https://github.com/active-hash/active_hash/compare/v0.7.4...v0.7.5
387 | [0.7.4]: https://github.com/active-hash/active_hash/compare/v0.7.3...v0.7.4
388 | [0.7.3]: https://github.com/active-hash/active_hash/compare/v0.7.2...v0.7.3
389 | [0.7.2]: https://github.com/active-hash/active_hash/compare/v0.7.0...v0.7.2
390 | [0.7.0]: https://github.com/active-hash/active_hash/compare/v0.6.1...v0.7.0
391 | [0.6.1]: https://github.com/active-hash/active_hash/compare/v0.6.0...v0.6.1
392 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "http://rubygems.org/"
2 |
3 | gemspec
4 |
5 | gem 'rspec', '~> 3.9'
6 | gem 'rake'
7 | gem 'test-unit'
8 | gem 'json'
9 |
10 | platforms :jruby do
11 | gem 'activerecord-jdbcsqlite3-adapter', '>= 1.3.6'
12 | end
13 |
14 | platforms :ruby do
15 | gem 'sqlite3', '~> 1.4', '< 2.0' # can allow 2.0 once Rails's sqlite adapter allows it
16 | end
17 |
18 | gem 'activerecord', '>= 6.1.0'
19 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2009 Jeff Dean
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.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ActiveHash
2 |
3 | [](https://github.com/active-hash/active_hash/actions/workflows/ruby.yml)
4 |
5 | ActiveHash is a simple base class that allows you to use a ruby hash as a readonly datasource for an ActiveRecord-like model.
6 |
7 | ActiveHash assumes that every hash has an :id key, which is what you would probably store in a database. This allows you to seamlessly upgrade from ActiveHash objects to full ActiveRecord objects without having to change any code in your app, or any foreign keys in your database.
8 |
9 | It also allows you to use #has_many and #belongs_to (via belongs_to_active_hash) in your AR objects.
10 |
11 | ActiveHash can also be useful to create simple test classes that run without a database - ideal for testing plugins or gems that rely on simple AR behavior, but don't want to deal with databases or migrations for the spec suite.
12 |
13 | ActiveHash also ships with:
14 |
15 | * ActiveFile: a base class that you can use to create file data sources
16 | * ActiveYaml: a base class that will turn YAML into a hash and load the data into an ActiveHash object
17 |
18 | ## !!! Important notice !!!
19 | We have changed returned value to chainable by v3.0.0. It's not just an `Array` instance anymore.
20 | If it breaks your application, please report us on [issues](https://github.com/active-hash/active_hash/issues), and use v2.x.x as following..
21 |
22 | ```ruby
23 | gem 'active_hash', '~> 2.3.0'
24 | ```
25 |
26 | ## Installation
27 |
28 | Bundler:
29 | ```ruby
30 | gem 'active_hash'
31 | ```
32 | Other:
33 | ```ruby
34 | gem install active_hash
35 | ```
36 |
37 | Requirements:
38 |
39 | - v1.x: Ruby >= 1.9.3, Rails >= 2.2.2
40 | - v2.x, v3.x: Ruby >= 2.4, Rails >= 5
41 | - upcoming: Ruby >= 3.0, Rails >= 6.1
42 |
43 | ```ruby
44 | gem 'active_hash', '~> 1.5.3'
45 | ```
46 |
47 | ## Reason for being
48 |
49 | We wrote ActiveHash so that we could use simple, in-memory, ActiveRecord-like data structures that play well with Rails forms, like:
50 | ```ruby
51 | # in app/models/country.rb
52 | class Country < ActiveHash::Base
53 | self.data = [
54 | {:id => 1, :name => "US"},
55 | {:id => 2, :name => "Canada"}
56 | ]
57 | end
58 |
59 | # in some view
60 | <%= collection_select :person, :country_id, Country.all, :id, :name %>
61 | ```
62 | Before ActiveHash, we did things like:
63 | ```ruby
64 | # in app/models/person.rb
65 | class Person < ActiveRecord::Base
66 | COUNTRIES = ["US", "Canada"]
67 | end
68 |
69 | # in some view
70 | <%= collection_select :person, :country_id, Person::COUNTRIES, :to_s, :to_s %>
71 | ```
72 | The majority of ActiveHash uses involve setting up some data at boot time, and never modifying that data at runtime.
73 |
74 | ## Usage
75 |
76 | To use ActiveHash, you need to:
77 |
78 | * Inherit from ActiveHash::Base
79 | * Define your data
80 | * Define your fields and/or default values
81 |
82 | A quick example would be:
83 | ```ruby
84 | class Country < ActiveHash::Base
85 | self.data = [
86 | {:id => 1, :name => "US"},
87 | {:id => 2, :name => "Canada"}
88 | ]
89 | end
90 |
91 | country = Country.new(:name => "Mexico")
92 | country.name # => "Mexico"
93 | country.name? # => true
94 | ```
95 | You can also use _create_:
96 | ```ruby
97 | class Country < ActiveHash::Base
98 | field :name
99 | create :id => 1, :name => "US"
100 | create :id => 2, :name => "Canada"
101 | end
102 | ```
103 | You can also use _add_:
104 | ```ruby
105 | class Country < ActiveHash::Base
106 | field :name
107 | add :id => 1, :name => "US"
108 | add :id => 2, :name => "Canada"
109 | end
110 | ```
111 | ## Auto-Defined fields
112 |
113 | ActiveHash will auto-define all fields for you when you load the hash. For example, if you have the following class:
114 | ```ruby
115 | class CustomField < ActiveHash::Base
116 | self.data = [
117 | {:custom_field_1 => "foo"},
118 | {:custom_field_2 => "foo"},
119 | {:custom_field_3 => "foo"}
120 | ]
121 | end
122 | ```
123 | Once you call CustomField.all it will define methods for :custom_field_1, :custom_field_2 etc...
124 |
125 | If you need the fields at load time, as opposed to after .all is called, you can also define them manually, like so:
126 | ```ruby
127 | class CustomField < ActiveHash::Base
128 | fields :custom_field_1, :custom_field_2, :custom_field_3
129 | end
130 | ```
131 | NOTE: auto-defined fields will _not_ override fields you've defined, either on the class or on the instance.
132 |
133 | ## Defining Fields with default values
134 |
135 | If some of your hash values contain nil, and you want to provide a default, you can specify defaults with the :field method:
136 | ```ruby
137 | class Country < ActiveHash::Base
138 | field :is_axis_of_evil, :default => false
139 | end
140 | ```
141 | ## Defining Data
142 |
143 | You can define data inside your class or outside. For example, you might have a class like this:
144 | ```ruby
145 | # app/models/country.rb
146 | class Country < ActiveHash::Base
147 | end
148 |
149 | # config/initializers/data.rb
150 | Rails.application.config.to_prepare do
151 | Country.data = [
152 | {:id => 1, :name => "US"},
153 | {:id => 2, :name => "Canada"}
154 | ]
155 | end
156 | ```
157 | If you prefer to store your data in YAML, see below.
158 |
159 | ## Class Methods
160 |
161 | ActiveHash gives you ActiveRecord-esque methods like:
162 | ```ruby
163 | Country.all # => returns all Country objects
164 | Country.count # => returns the length of the .data array
165 | Country.first # => returns the first country object
166 | Country.last # => returns the last country object
167 | Country.find 1 # => returns the first country object with that id
168 | Country.find [1,2] # => returns all Country objects with ids in the array
169 | Country.find :all # => same as .all
170 | Country.find :all, args # => the second argument is totally ignored, but allows it to play nicely with AR
171 | Country.find { |country| country.name.start_with?('U') } # => returns the first country for which the block evaluates to true
172 | Country.find_by_id 1 # => find the first object that matches the id
173 | Country.find_by(name: 'US') # => returns the first country object with specified argument
174 | Country.find_by!(name: 'US') # => same as find_by, but raise exception when not found
175 | Country.where(name: 'US') # => returns all records with name: 'US'
176 | Country.where(name: /U/) # => returns all records where the name matches the regex /U/
177 | Country.where.not(name: 'US') # => returns all records without name: 'US'
178 | Country.order(name: :desc) # => returns all records ordered by name attribute in DESC order
179 | ```
180 | It also gives you a few dynamic finder methods. For example, if you defined :name as a field, you'd get:
181 | ```ruby
182 | Country.find_by_name "foo" # => returns the first object matching that name
183 | Country.find_all_by_name "foo" # => returns an array of the objects with matching names
184 | Country.find_by_id_and_name 1, "Germany" # => returns the first object matching that id and name
185 | Country.find_all_by_id_and_name 1, "Germany" # => returns an array of objects matching that name and id
186 | ```
187 |
188 | Furthermore, it allows to create custom scope query methods, similar to how it's possible with ActiveRecord:
189 |
190 | ```ruby
191 | Country.scope :english, -> { where(language: 'English') } # Creates a class method Country.english performing the given query
192 | Country.scope :with_language, ->(language) { where(language: language) } # Creates a class method Country.with_language(language) performing the given query
193 | ```
194 |
195 | ## Instance Methods
196 |
197 | ActiveHash objects implement enough of the ActiveRecord api to satisfy most common needs. For example:
198 | ```
199 | Country#id # => returns the id or nil
200 | Country#id= # => sets the id attribute
201 | Country#quoted_id # => returns the numeric id
202 | Country#to_param # => returns the id as a string
203 | Country#new_record? # => returns true if is not part of Country.all, false otherwise
204 | Country#readonly? # => true
205 | Country#hash # => the hash of the id (or the hash of nil)
206 | Country#eql? # => compares type and id, returns false if id is nil
207 | ```
208 | ActiveHash also gives you methods related to the fields you defined. For example, if you defined :name as a field, you'd get:
209 | ```
210 | Country#name # => returns the passed in name
211 | Country#name? # => returns true if the name is not blank
212 | Country#name= # => sets the name
213 | ```
214 | ## Saving in-memory records
215 |
216 | The ActiveHash::Base.all method functions like an in-memory data store. You can save your records as ActiveHash::Relation object by using standard ActiveRecord create and save methods:
217 | ```ruby
218 | Country.all
219 | => #
220 | Country.create
221 | => #1}>
222 | Country.all
223 | => #1}>], @conditions=[..], @records_dirty=false>
224 | country = Country.new
225 | => #
226 | country.new_record?
227 | => true
228 | country.save
229 | => true
230 | country.new_record?
231 | # => false
232 | Country.all
233 | => #1}>, #2}>], @conditions=[..], @records_dirty=false>
234 | ```
235 | Notice that when adding records to the collection, it will auto-increment the id for you by default. If you use string ids, it will not auto-increment the id. Available methods are:
236 | ```
237 | Country.insert( record )
238 | Country#save
239 | Country#save!
240 | Country.create
241 | Country.create!
242 | ```
243 | As such, ActiveHash::Base and its descendants should work with Fixjour or FactoryBot, so you can treat ActiveHash records the same way you would any other ActiveRecord model in tests.
244 |
245 | To clear all records from the in-memory array, call delete_all:
246 | ```ruby
247 | Country.delete_all # => does not affect the yaml files in any way - just clears the in-memory array which can be useful for testing
248 | ```
249 | ## Referencing ActiveHash objects from ActiveRecord Associations
250 |
251 | One common use case for ActiveHash is to have top-level objects in memory that ActiveRecord objects belong to.
252 |
253 | ```ruby
254 | class Country < ActiveHash::Base
255 | end
256 |
257 | class Person < ActiveRecord::Base
258 | extend ActiveHash::Associations::ActiveRecordExtensions
259 | belongs_to :country
260 | end
261 | ```
262 | NOTE: this needs to be called on a subclass of ActiveRecord::Base. If you extend ActiveRecord::Base, it will not work.
263 | If you want to extend ActiveRecord::Base so all your AR models can belong to ActiveHash::Base objects, you can use the
264 | `belongs_to_active_hash` method:
265 | ```ruby
266 | ActiveRecord::Base.extend ActiveHash::Associations::ActiveRecordExtensions
267 |
268 | class Country < ActiveHash::Base
269 | end
270 |
271 | class Person < ActiveRecord::Base
272 | belongs_to_active_hash :country
273 | end
274 | ```
275 |
276 | ### Using shortcuts
277 |
278 | Since ActiveHashes usually are static, we can use shortcuts to assign via an easy to remember string instead of an obscure ID number.
279 | ```ruby
280 | # app/models/country.rb
281 | class Country < ActiveHash::Base
282 | end
283 |
284 | # app/models/person.rb
285 | class Person < ActiveRecord::Base
286 | extend ActiveHash::Associations::ActiveRecordExtensions
287 | belongs_to_active_hash :country, :shortcuts => [:name]
288 | end
289 |
290 | # config/initializers/data.rb
291 | Rails.application.config.to_prepare do
292 | Country.data = [
293 | {:id => 1, :name => "US"},
294 | {:id => 2, :name => "Canada"}
295 | ]
296 | end
297 |
298 | # Using `rails console`
299 | john = Person.new
300 | john.country_name = "US"
301 | # Is the same as doing `john.country = Country.find_by_name("US")`
302 | john.country_name
303 | # Will return "US", and is the same as doing `john.country.try(:name)`
304 | ```
305 | You can have multiple shortcuts, so settings `:shortcuts => [:name, :friendly_name]` will enable you to use `#country_name=` and `#country_friendly_name=`.
306 |
307 | ## Referencing ActiveRecord objects from ActiveHash
308 |
309 | If you include the ActiveHash::Associations module, you can also create associations from your ActiveHash classes, like so:
310 | ```ruby
311 | class Country < ActiveHash::Base
312 | include ActiveHash::Associations
313 | has_many :people
314 | end
315 |
316 | class Person < ActiveHash::Base
317 | include ActiveHash::Associations
318 | belongs_to :country
319 | has_many :pets
320 | end
321 |
322 | class Pet < ActiveRecord::Base
323 | end
324 | ```
325 | Once you define a belongs to, you also get the setter method:
326 | ```ruby
327 | class City < ActiveHash::Base
328 | include ActiveHash::Associations
329 | belongs_to :state
330 | end
331 |
332 | city = City.new
333 | city.state = State.first
334 | city.state_id # is State.first.id
335 | ```
336 | NOTE: You cannot use ActiveHash objects as children of ActiveRecord and I don't plan on adding support for that. It doesn't really make any sense, since you'd have to hard-code your database ids in your class or yaml files, which is a dependency inversion.
337 |
338 | Thanks to baldwindavid for the ideas and code on that one.
339 |
340 | ## ActiveYaml
341 |
342 | If you want to store your data in YAML files, just inherit from ActiveYaml and specify your path information:
343 | ```ruby
344 | class Country < ActiveYaml::Base
345 | end
346 | ```
347 | By default, this class will look for a yml file named "countries.yml" in the same directory as the file. You can either change the directory it looks in, the filename it looks for, or both:
348 | ```ruby
349 | class Country < ActiveYaml::Base
350 | set_root_path "/u/data"
351 | set_filename "sample"
352 | end
353 | ```
354 | The above example will look for the file "/u/data/sample.yml".
355 |
356 | Since ActiveYaml just creates a hash from the YAML file, you will have all fields specified in YAML auto-defined for you. You can format your YAML as an array, or as a hash:
357 | ```yaml
358 | # array style
359 | - id: 1
360 | name: US
361 | - id: 2
362 | name: Canada
363 | - id: 3
364 | name: Mexico
365 |
366 | # hash style
367 | us:
368 | id: 1
369 | name: US
370 | canada:
371 | id: 2
372 | name: Canada
373 | mexico:
374 | id: 3
375 | name: Mexico
376 | ```
377 |
378 | ### Automatic Key Attribute
379 |
380 | When using the hash format for your YAML file, ActiveYaml will automatically add a `key` attribute with the name of the object. You can overwrite this by setting the key attribute in the YAML file.
381 | For example:
382 | ```
383 | au:
384 | id: 1
385 | name: Australia
386 | ```
387 |
388 | When you access the object you can do `Country.find(1).key => 'au'`. Or `Country.find_by_key('au')`
389 |
390 | If you want a different key on only some objects you can mix and match:
391 |
392 | ```
393 | au:
394 | id: 1
395 | key: aus
396 | name: Australia
397 | nz:
398 | id: 2
399 | name: New Zealand
400 | ```
401 |
402 | `Country.find(1).key => 'aus'`
403 |
404 | `Country.find(2).key => 'nz'`
405 |
406 | ### Multiple files per model
407 |
408 | You can use multiple files to store your data. You will have to choose between hash or array style as you cannot use both for one model.
409 | ```ruby
410 | class Country < ActiveYaml::Base
411 | use_multiple_files
412 | set_filenames "europe", "america", "asia", "africa"
413 | end
414 | ```
415 | ### Using aliases in YAML
416 |
417 | Aliases can be used in ActiveYaml using either array or hash style by including `ActiveYaml::Aliases`.
418 | With that module included, keys beginning with a '/' character can be safely added, and will be ignored, allowing you to add aliases anywhere in your code:
419 | ```yaml
420 | # Array Style
421 | - /aliases:
422 | soda_flavor: &soda_flavor
423 | sweet
424 | soda_price: &soda_price
425 | 1.0
426 |
427 | - id: 1
428 | name: Coke
429 | flavor: *soda_flavor
430 | price: *soda_price
431 |
432 |
433 | # Key style
434 | /aliases:
435 | soda_flavor: &soda_flavor
436 | sweet
437 | soda_price: &soda_price
438 | 1.0
439 |
440 | coke:
441 | id: 1
442 | name: Coke
443 | flavor: *soda_flavor
444 | price: *soda_price
445 | ```
446 | ```ruby
447 | class Soda < ActiveYaml::Base
448 | include ActiveYaml::Aliases
449 | end
450 |
451 | Soda.length # => 1
452 | Soda.first.flavor # => sweet
453 | Soda.first.price # => 1.0
454 | ```
455 |
456 | ### Using ERB ruby in YAML
457 |
458 | Embedded ruby can be used in ActiveYaml using erb brackets `<% %>` and `<%= %>` to set the result of a ruby operation as a value in the yaml file.
459 |
460 | ```yaml
461 | - id: 1
462 | email: <%= "user#{rand(100)}@email.com" %>
463 | password: <%= ENV['USER_PASSWORD'] %>
464 | ```
465 |
466 | This can be disabled in an initializer:
467 | ```ruby
468 | # config/initializers/active_yaml.rb
469 | ActiveYaml::Base.process_erb = false
470 | ```
471 |
472 | ## ActiveJSON
473 |
474 | If you want to store your data in JSON files, just inherit from ActiveJSON and specify your path information:
475 | ```ruby
476 | class Country < ActiveJSON::Base
477 | end
478 | ```
479 | By default, this class will look for a json file named "countries.json" in the same directory as the file. You can either change the directory it looks in, the filename it looks for, or both:
480 | ```ruby
481 | class Country < ActiveJSON::Base
482 | set_root_path "/u/data"
483 | set_filename "sample"
484 | end
485 | ```
486 | The above example will look for the file "/u/data/sample.json".
487 |
488 | Since ActiveJSON just creates a hash from the JSON file, you will have all fields specified in JSON auto-defined for you. You can format your JSON as an array, or as a hash:
489 | ```ruby
490 | # array style
491 | [
492 | {
493 | "id": 1,
494 | "name": "US",
495 | "custom_field_1": "value1"
496 | },
497 | {
498 | "id": 2,
499 | "name": "Canada",
500 | "custom_field_2": "value2"
501 | }
502 | ]
503 |
504 | # hash style
505 | {
506 | { "us":
507 | {
508 | "id": 1,
509 | "name": "US",
510 | "custom_field_1": "value1"
511 | }
512 | },
513 | { "canada":
514 | {
515 | "id": 2,
516 | "name": "Canada",
517 | "custom_field_2": "value2"
518 | }
519 | }
520 | }
521 | ```
522 | ### Multiple files per model
523 |
524 | This works as it does for `ActiveYaml`
525 |
526 | ## ActiveFile
527 |
528 | If you store encrypted data, or you'd like to store your flat files as CSV or XML or any other format, you can easily include ActiveHash to parse and load your file. Just add a custom ::load_file method, and define the extension you want the file to use:
529 | ```ruby
530 | class Country < ActiveFile::Base
531 | set_root_path "/u/data"
532 | set_filename "sample"
533 |
534 | class << self
535 | def extension
536 | "super_secret"
537 | end
538 |
539 | def load_file
540 | MyAwesomeDecoder.load_file(full_path)
541 | end
542 | end
543 | end
544 | ```
545 | The two methods you need to implement are load_file, which needs to return an array of hashes, and .extension, which returns the file extension you are using. You have full_path available to you if you wish, or you can provide your own path.
546 |
547 | Setting the default file location in Rails:
548 | ```ruby
549 | # config/initializers/active_file.rb
550 | ActiveFile::Base.set_root_path "config/activefiles"
551 | ```
552 | In Rails, in development mode, it reloads the entire class, which reloads the file. In production, the data cached in memory.
553 |
554 | NOTE: By default, .full_path refers to the current working directory. In a rails app, this will be RAILS_ROOT.
555 |
556 |
557 | ## Reloading ActiveYaml, ActiveJSON and ActiveFile
558 |
559 | During the development you may often change your data and want to see your changes immediately.
560 | Call `Model.reload(true)` to force reload the data from disk.
561 |
562 | In Rails, you can use this snippet. Please just note it resets the state every request, which may not always be desired.
563 |
564 | ```ruby
565 | before_action do
566 | [Model1, Model2, Model3].each { |m| m.reload(true) }
567 | end
568 | ```
569 |
570 | ## Enum
571 |
572 | ActiveHash can expose its data in an Enumeration by setting constants for each record. This allows records to be accessed in code through a constant set in the ActiveHash class.
573 |
574 | The field to be used as the constant is set using _enum_accessor_ which takes the name of a field as an argument.
575 | ```ruby
576 | class Country < ActiveHash::Base
577 | include ActiveHash::Enum
578 | self.data = [
579 | {:id => 1, :name => "US", :capital => "Washington, DC"},
580 | {:id => 2, :name => "Canada", :capital => "Ottawa"},
581 | {:id => 3, :name => "Mexico", :capital => "Mexico City"}
582 | ]
583 | enum_accessor :name
584 | end
585 | ```
586 | Records can be accessed by looking up the field constant:
587 |
588 | >> Country::US.capital
589 | => "Washington DC"
590 | >> Country::MEXICO.id
591 | => 3
592 | >> Country::CANADA
593 | => #"Canada", :id=>2}
594 |
595 | You may also use multiple attributes to generate the constant, like so:
596 | ```ruby
597 | class Town < ActiveHash::Base
598 | include ActiveHash::Enum
599 | self.data = [
600 | {:id => 1, :name => "Columbus", :state => "NY"},
601 | {:id => 2, :name => "Columbus", :state => "OH"}
602 | ]
603 | enum_accessor :name, :state
604 | end
605 |
606 | >> Town::COLUMBUS_NY
607 | >> Town::COLUMBUS_OH
608 | ```
609 | Constants are formed by first stripping all non-word characters and then upcasing the result. This means strings like "Blazing Saddles", "ReBar", "Mike & Ike" and "Ho! Ho! Ho!" become BLAZING_SADDLES, REBAR, MIKE_IKE and HO_HO_HO.
610 |
611 | The field specified as the _enum_accessor_ must contain unique data values.
612 |
613 | ## I18n
614 |
615 | ActiveHash supports i18n as ActiveModel.
616 | Put following code in one of your locale file (e.g. `config/locales/LANGUAGE_CODE.yml`)
617 |
618 | ```yaml
619 | # for example, inside config/locales/ja.yml
620 | ja:
621 | activemodel:
622 | models:
623 | # `Country.model_name.human` will evaluates to "国"
624 | country: "国"
625 | ```
626 |
627 | ## Contributing
628 |
629 | If you'd like to become an ActiveHash contributor, the easiest way it to fork this repo, make your changes, run the specs and submit a pull request once they pass.
630 |
631 | To run specs, run:
632 |
633 | bundle install
634 | bundle exec rspec spec
635 |
636 | If your changes seem reasonable and the specs pass I'll give you commit rights to this repo and add you to the list of people who can push the gem.
637 |
638 | ## Releasing a new version
639 |
640 | To make users' lives easier, CI will exercise tests for:
641 |
642 | * Ruby 3.0 through current
643 | * ActiveRecord/ActiveSupport from 6.1 through edge
644 |
645 | Once appraisal passes in all supported rubies, follow these steps to release a new version of active_hash:
646 |
647 | * update the changelog with a brief summary of the changes that are included in the release
648 | * bump the gem version by editing the `version.rb` file
649 | * if there are new contributors, add them to the list of authors in the gemspec
650 | * run `rake build`
651 | * commit those changes
652 | * run `rake install` and verify that the gem loads correctly from an irb session
653 | * run `rake release`, which will rebuild the gem, tag it, push the tags (and your latest commit) to github, then push the gem to rubygems.org
654 |
655 | If you have any questions about how to maintain backwards compatibility, please email me and we can figure it out.
656 |
657 | ## Copyright
658 |
659 | Copyright (c) 2010 Jeff Dean. See LICENSE for details.
660 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler/setup'
2 | require 'bundler/gem_tasks'
3 |
4 | require 'rspec/core/rake_task'
5 | RSpec::Core::RakeTask.new(:spec) do |spec|
6 | spec.pattern = 'spec/**/*_spec.rb'
7 | end
8 |
9 | task :default => :spec
10 |
--------------------------------------------------------------------------------
/active_hash.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 |
3 | $:.push File.expand_path("../lib", __FILE__)
4 | require "active_hash/version"
5 |
6 | Gem::Specification.new do |s|
7 | s.name = "active_hash"
8 | s.version = ActiveHash::Gem::VERSION
9 | s.authors = [
10 | "Jeff Dean",
11 | "Mike Dalessio",
12 | "Corey Innis",
13 | "Peter Jaros",
14 | "Brandon Keene",
15 | "Brian Takita",
16 | "Pat Nakajima",
17 | "John Pignata",
18 | "Michael Schubert",
19 | "Jeremy Weiskotten",
20 | "Ryan Garver",
21 | "Tom Stuart",
22 | "Joel Chippindale",
23 | "Kevin Olsen",
24 | "Vladimir Andrijevik",
25 | "Adam Anderson",
26 | "Keenan Brock",
27 | "Desmond Bowe",
28 | "Matthew O'Riordan",
29 | "Brett Richardson",
30 | "Rachel Heaton",
31 | "Keisuke Izumiya"
32 | ]
33 | s.email = %q{jeff@zilkey.com}
34 | s.summary = %q{An ActiveRecord-like model that uses a hash or file as a datasource}
35 | s.description = %q{Includes the ability to specify data using hashes, yml files or JSON files}
36 | s.homepage = %q{http://github.com/active-hash/active_hash}
37 | s.license = "MIT"
38 |
39 | s.metadata = {
40 | "homepage_uri" => s.homepage,
41 | "changelog_uri" => "https://github.com/active-hash/active_hash/blob/master/CHANGELOG.md",
42 | "source_code_uri" => s.homepage,
43 | "bug_tracker_uri" => "https://github.com/active-hash/active_hash/issues",
44 | }
45 |
46 | s.files = [
47 | "CHANGELOG.md",
48 | "LICENSE",
49 | "README.md",
50 | "active_hash.gemspec",
51 | Dir.glob("lib/**/*")
52 | ].flatten
53 | s.test_files = s.files.grep(%r{^(test|spec|features)/})
54 | s.add_runtime_dependency('activesupport', '>= 6.1.0')
55 | s.add_development_dependency "pry"
56 | s.required_ruby_version = '>= 3.0.0'
57 | end
58 |
--------------------------------------------------------------------------------
/gemfiles/rails_6.1.gemfile:
--------------------------------------------------------------------------------
1 | source 'http://rubygems.org/'
2 |
3 | gem 'activerecord', '~> 6.1.0'
4 | gem 'rspec', '~> 3.9'
5 | gem 'rake', '~> 13.0'
6 | gem 'json'
7 | gem 'test-unit'
8 | gem 'concurrent-ruby', '< 1.3.5' # to avoid problem described in https://github.com/rails/rails/pull/54264
9 |
10 | platform :jruby do
11 | gem 'activerecord-jdbcsqlite3-adapter', '>= 1.3.6'
12 | end
13 |
14 | platform :ruby do
15 | gem 'sqlite3', '~> 1.4', '< 2.0' # can allow 2.0 once Rails's sqlite adapter allows it
16 | end
17 |
18 | gemspec :path => '../'
19 |
--------------------------------------------------------------------------------
/gemfiles/rails_7.0.gemfile:
--------------------------------------------------------------------------------
1 | source 'http://rubygems.org/'
2 |
3 | gem 'activerecord', '~> 7.0.0'
4 | gem 'rspec', '~> 3.9'
5 | gem 'rake', '~> 13.0'
6 | gem 'json'
7 | gem 'test-unit'
8 | gem 'concurrent-ruby', '< 1.3.5' # to avoid problem described in https://github.com/rails/rails/pull/54264
9 |
10 | platform :jruby do
11 | gem 'activerecord-jdbcsqlite3-adapter', '>= 1.3.6'
12 | end
13 |
14 | platform :ruby do
15 | gem 'sqlite3', '~> 1.4', '< 2.0' # can allow 2.0 once Rails's sqlite adapter allows it
16 | end
17 |
18 | gemspec :path => '../'
19 |
--------------------------------------------------------------------------------
/gemfiles/rails_7.1.gemfile:
--------------------------------------------------------------------------------
1 | source 'http://rubygems.org/'
2 |
3 | gem 'activerecord', '~> 7.1.0'
4 | gem 'rspec', '~> 3.9'
5 | gem 'rake', '~> 13.0'
6 | gem 'json'
7 | gem 'test-unit'
8 |
9 | platform :jruby do
10 | gem 'activerecord-jdbcsqlite3-adapter', '>= 1.3.6'
11 | end
12 |
13 | platform :ruby do
14 | gem 'sqlite3', '~> 1.4', '< 2.0' # can allow 2.0 once Rails's sqlite adapter allows it
15 | end
16 |
17 | gemspec :path => '../'
18 |
--------------------------------------------------------------------------------
/gemfiles/rails_7.2.gemfile:
--------------------------------------------------------------------------------
1 | source 'http://rubygems.org/'
2 |
3 | gem 'activerecord', '~> 7.2.0'
4 | gem 'rspec', '~> 3.9'
5 | gem 'rake', '~> 13.0'
6 | gem 'json'
7 | gem 'test-unit'
8 |
9 | platform :jruby do
10 | gem 'activerecord-jdbcsqlite3-adapter', '>= 1.3.6'
11 | end
12 |
13 | platform :ruby do
14 | gem 'sqlite3', '~> 1.4', '< 2.0' # can allow 2.0 once Rails's sqlite adapter allows it
15 | end
16 |
17 | gemspec :path => '../'
18 |
--------------------------------------------------------------------------------
/gemfiles/rails_8.0.gemfile:
--------------------------------------------------------------------------------
1 | source 'http://rubygems.org/'
2 |
3 | gem 'activerecord', '~> 8.0.0.beta1'
4 | gem 'rspec', '~> 3.13'
5 | gem 'rake', '~> 13.2'
6 | gem 'json'
7 | gem 'test-unit'
8 |
9 | platform :jruby do
10 | gem 'activerecord-jdbcsqlite3-adapter', '>= 1.3.6'
11 | end
12 |
13 | platform :ruby do
14 | gem 'sqlite3'
15 | end
16 |
17 | gemspec :path => '../'
18 |
--------------------------------------------------------------------------------
/lib/active_file/base.rb:
--------------------------------------------------------------------------------
1 | module ActiveFile
2 |
3 | class Base < ActiveHash::Base
4 | extend ActiveFile::MultipleFiles
5 | @@instance_lock = Mutex.new
6 |
7 | class_attribute :filename, :root_path, :data_loaded, instance_reader: false, instance_writer: false
8 |
9 | class << self
10 |
11 | def delete_all
12 | self.data_loaded = true
13 | super
14 | end
15 |
16 | def reload(force = false)
17 | @@instance_lock.synchronize do
18 | return if !self.dirty && !force && self.data_loaded
19 | self.data = load_file
20 | mark_clean
21 | self.data_loaded = true
22 | end
23 | end
24 |
25 | def set_filename(name)
26 | self.filename = name
27 | end
28 |
29 | def set_root_path(path)
30 | self.root_path = path
31 | end
32 |
33 | def load_file
34 | raise "Override Me"
35 | end
36 |
37 | def full_path
38 | actual_filename = filename || name.tableize
39 | File.join(actual_root_path, "#{actual_filename}.#{extension}")
40 | end
41 |
42 | def extension
43 | raise "Override Me"
44 | end
45 | protected :extension
46 |
47 | def actual_root_path
48 | root_path || Dir.pwd
49 | end
50 | protected :actual_root_path
51 |
52 | [:find, :find_by_id, :all, :where, :method_missing].each do |method|
53 | define_method(method) do |*args, &block|
54 | reload unless data_loaded
55 | return super(*args, &block)
56 | end
57 | end
58 |
59 | def all_in_process
60 | return super if data_loaded
61 | @records || []
62 | end
63 | end
64 | end
65 |
66 | end
67 |
--------------------------------------------------------------------------------
/lib/active_file/hash_and_array_files.rb:
--------------------------------------------------------------------------------
1 | module ActiveFile
2 | module HashAndArrayFiles
3 | def raw_data
4 | if multiple_files?
5 | data_from_multiple_files
6 | else
7 | load_path(full_path)
8 | end
9 | end
10 |
11 | private
12 | def data_from_multiple_files
13 | loaded_files = full_paths.collect { |path| load_path(path) }
14 |
15 | if loaded_files.all?{ |file_data| file_data.is_a?(Array) }
16 | loaded_files.sum([])
17 | elsif loaded_files.all?{ |file_data| file_data.is_a?(Hash) }
18 | loaded_files.inject({}) { |hash, file_data| hash.merge(file_data) }
19 | else
20 | raise ActiveHash::FileTypeMismatchError.new("Choose between hash or array syntax")
21 | end
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/active_file/multiple_files.rb:
--------------------------------------------------------------------------------
1 | module ActiveFile
2 | module MultipleFiles
3 | def multiple_files?
4 | false
5 | end
6 |
7 | def use_multiple_files
8 | class_attribute :filenames, instance_reader: false, instance_writer: false
9 |
10 | def self.set_filenames(*filenames)
11 | self.filenames = filenames
12 | end
13 |
14 | def self.multiple_files?
15 | true
16 | end
17 |
18 | def self.full_paths
19 | if filenames.present?
20 | filenames.collect do |filename|
21 | File.join(actual_root_path, "#{filename}.#{extension}")
22 | end
23 | else
24 | [full_path]
25 | end
26 | end
27 | end
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/active_hash.rb:
--------------------------------------------------------------------------------
1 | require 'active_support'
2 |
3 | begin
4 | require 'active_support/core_ext'
5 | rescue
6 | end
7 |
8 | begin
9 | require 'active_model'
10 | require 'active_model/naming'
11 | rescue LoadError
12 | end
13 |
14 | require 'active_hash/base'
15 | require 'active_hash/relation'
16 | require 'active_hash/condition'
17 | require 'active_hash/conditions'
18 | require 'active_file/multiple_files'
19 | require 'active_file/hash_and_array_files'
20 | require 'active_file/base'
21 | require 'active_yaml/base'
22 | require 'active_yaml/aliases'
23 | require 'active_json/base'
24 | require 'associations/associations'
25 | require 'enum/enum'
26 |
--------------------------------------------------------------------------------
/lib/active_hash/base.rb:
--------------------------------------------------------------------------------
1 | module ActiveHash
2 | class RecordNotFound < StandardError
3 | attr_reader :model, :primary_key, :id
4 |
5 | def initialize(message = nil, model = nil, primary_key = nil, id = nil)
6 | @primary_key = primary_key
7 | @model = model
8 | @id = id
9 |
10 | super(message)
11 | end
12 | end
13 |
14 | class ReservedFieldError < StandardError
15 | end
16 |
17 | class IdError < StandardError
18 | end
19 |
20 | class FileTypeMismatchError < StandardError
21 | end
22 |
23 | class Base
24 | class_attribute :_data, :dirty, :default_attributes, :scopes
25 |
26 | if Object.const_defined?(:ActiveModel)
27 | extend ActiveModel::Translation
28 | include ActiveModel::Conversion
29 | else
30 | def to_param
31 | id.present? ? id.to_s : nil
32 | end
33 | end
34 |
35 | class << self
36 |
37 | def cache_key
38 | if Object.const_defined?(:ActiveModel)
39 | model_name.cache_key
40 | else
41 | ActiveSupport::Inflector.tableize(self.name).downcase
42 | end
43 | end
44 |
45 | def primary_key
46 | "id"
47 | end
48 |
49 | def field_names
50 | @field_names ||= []
51 | end
52 |
53 | #
54 | # Useful for CSV integration needing column names as strings.
55 | #
56 | # @return [Array] An array of column names as strings.
57 | #
58 | # @example Usage
59 | # class Country < ActiveHash::Base
60 | # fields :name, :code
61 | # end
62 | #
63 | # Country.column_names
64 | # # => ["id", "name", "code"]
65 | #
66 | def column_names
67 | field_names.map(&:name)
68 | end
69 |
70 | def the_meta_class
71 | class << self
72 | self
73 | end
74 | end
75 |
76 | def compute_type(type_name)
77 | self
78 | end
79 |
80 | def pluralize_table_names
81 | true
82 | end
83 |
84 | def empty?
85 | false
86 | end
87 |
88 | def data
89 | _data
90 | end
91 |
92 | def data=(array_of_hashes)
93 | mark_dirty
94 | @records = nil
95 | reset_record_index
96 | self._data = array_of_hashes
97 | if array_of_hashes
98 | auto_assign_fields(array_of_hashes)
99 | array_of_hashes.each do |hash|
100 | insert new(hash)
101 | end
102 | end
103 | end
104 |
105 | def exists?(args = :none)
106 | if args.respond_to?(:id)
107 | record_index[args.id.to_s].present?
108 | elsif !args
109 | false
110 | elsif args == :none
111 | all.present?
112 | elsif args.is_a?(Hash)
113 | all.where(args).present?
114 | else
115 | all.where(id: args.to_i).present?
116 | end
117 | end
118 |
119 | def insert(record)
120 | @records ||= []
121 | record[:id] ||= next_id
122 | validate_unique_id(record) if dirty
123 | mark_dirty
124 |
125 | add_to_record_index({ record.id.to_s => @records.length })
126 | @records << record
127 | end
128 |
129 | def next_id
130 | max_record = all_in_process.max { |a, b| a.id <=> b.id }
131 | if max_record.nil?
132 | 1
133 | elsif max_record.id.is_a?(Numeric)
134 | max_record.id.succ
135 | end
136 | end
137 |
138 | def all_in_process
139 | all
140 | end
141 | private :all_in_process
142 |
143 | def record_index
144 | @record_index ||= {}
145 | end
146 |
147 | def has_query_constraints?
148 | false
149 | end
150 |
151 | private :record_index
152 |
153 | def reset_record_index
154 | record_index.clear
155 | end
156 |
157 | private :reset_record_index
158 |
159 | def add_to_record_index(entry)
160 | record_index.merge!(entry)
161 | end
162 |
163 | private :add_to_record_index
164 |
165 | def validate_unique_id(record)
166 | raise IdError.new("Duplicate ID found for record #{record.attributes.inspect}") if record_index.has_key?(record.id.to_s)
167 | end
168 |
169 | private :validate_unique_id
170 |
171 | def create(attributes = {})
172 | record = new(attributes)
173 | record.save
174 | mark_dirty
175 | record
176 | end
177 |
178 | alias_method :add, :create
179 |
180 | def create!(attributes = {})
181 | record = new(attributes)
182 | record.save!
183 | record
184 | end
185 |
186 | def all(options = {})
187 | relation = ActiveHash::Relation.new(self, @records || [])
188 | relation = relation.where!(options[:conditions]) if options[:conditions]
189 | relation
190 | end
191 |
192 | delegate :where, :find, :find_by, :find_by!, :find_by_id, :count, :pluck, :ids, :pick, :first, :last, :order, to: :all
193 |
194 | def transaction
195 | yield
196 | rescue LocalJumpError => err
197 | raise err
198 | rescue StandardError => e
199 | unless Object.const_defined?(:ActiveRecord) && e.is_a?(ActiveRecord::Rollback)
200 | raise e
201 | end
202 | end
203 |
204 | def delete_all
205 | mark_dirty
206 | reset_record_index
207 | @records = []
208 | end
209 |
210 | def fields(*args)
211 | options = args.extract_options!
212 | args.each do |field|
213 | field(field, options)
214 | end
215 | end
216 |
217 | def field(field_name, options = {})
218 | field_name = field_name.to_sym
219 | validate_field(field_name)
220 |
221 | field_names << field_name
222 |
223 | add_default_value(field_name, options[:default]) if options.key?(:default)
224 | define_getter_method(field_name, options[:default])
225 | define_setter_method(field_name)
226 | define_interrogator_method(field_name)
227 | define_custom_find_method(field_name)
228 | define_custom_find_all_method(field_name)
229 | end
230 |
231 | def validate_field(field_name)
232 | field_name = field_name.to_sym
233 | if [:attributes].include?(field_name)
234 | raise ReservedFieldError.new("#{field_name} is a reserved field in ActiveHash. Please use another name.")
235 | end
236 | end
237 |
238 | private :validate_field
239 |
240 | def respond_to?(method_name, include_private=false)
241 | super ||
242 | begin
243 | config = configuration_for_custom_finder(method_name)
244 | config && config[:fields].all? do |field|
245 | field_names.include?(field.to_sym) || field.to_sym == :id
246 | end
247 | end
248 | end
249 |
250 | def method_missing(method_name, *args)
251 | return super unless respond_to? method_name
252 |
253 | config = configuration_for_custom_finder(method_name)
254 | attribute_pairs = config[:fields].zip(args)
255 | matches = all.select { |base| attribute_pairs.all? { |field, value| base.send(field).to_s == value.to_s } }
256 |
257 | if config[:all?]
258 | matches
259 | else
260 | result = matches.first
261 | if config[:bang?]
262 | result || raise(RecordNotFound, "Couldn\'t find #{name} with #{attribute_pairs.collect { |pair| "#{pair[0]} = #{pair[1]}" }.join(', ')}")
263 | else
264 | result
265 | end
266 | end
267 | end
268 |
269 | def configuration_for_custom_finder(finder_name)
270 | if finder_name.to_s.match(/^find_(all_)?by_(.*?)(!)?$/) && !($1 && $3)
271 | {
272 | :all? => !!$1,
273 | :bang? => !!$3,
274 | :fields => $2.split('_and_')
275 | }
276 | end
277 | end
278 |
279 | private :configuration_for_custom_finder
280 |
281 | def add_default_value field_name, default_value
282 | self.default_attributes ||= {}
283 | self.default_attributes[field_name] = default_value
284 | end
285 |
286 | def define_getter_method(field, default_value)
287 | unless instance_methods.include?(field)
288 | define_method(field) do
289 | attributes[field].nil? ? default_value : attributes[field]
290 | end
291 | end
292 | end
293 |
294 | private :define_getter_method
295 |
296 | def define_setter_method(field)
297 | method_name = :"#{field}="
298 | unless instance_methods.include?(method_name)
299 | define_method(method_name) do |new_val|
300 | @attributes[field] = new_val
301 | end
302 | end
303 | end
304 |
305 | private :define_setter_method
306 |
307 | def define_interrogator_method(field)
308 | method_name = :"#{field}?"
309 | unless instance_methods.include?(method_name)
310 | define_method(method_name) do
311 | send(field).present?
312 | end
313 | end
314 | end
315 |
316 | private :define_interrogator_method
317 |
318 | def define_custom_find_method(field_name)
319 | method_name = :"find_by_#{field_name}"
320 | unless singleton_methods.include?(method_name)
321 | the_meta_class.instance_eval do
322 | define_method(method_name) do |*args|
323 | args.extract_options!
324 | identifier = args[0]
325 | all.detect { |record| record.send(field_name) == identifier }
326 | end
327 | end
328 | end
329 | end
330 |
331 | private :define_custom_find_method
332 |
333 | def define_custom_find_all_method(field_name)
334 | method_name = :"find_all_by_#{field_name}"
335 | unless singleton_methods.include?(method_name)
336 | the_meta_class.instance_eval do
337 | unless singleton_methods.include?(method_name)
338 | define_method(method_name) do |*args|
339 | args.extract_options!
340 | identifier = args[0]
341 | all.select { |record| record.send(field_name) == identifier }
342 | end
343 | end
344 | end
345 | end
346 | end
347 |
348 | private :define_custom_find_all_method
349 |
350 | def auto_assign_fields(array_of_hashes)
351 | (array_of_hashes || []).inject([]) do |array, row|
352 | row.symbolize_keys!
353 | row.keys.each do |key|
354 | unless key.to_s == "id"
355 | array << key
356 | end
357 | end
358 | array
359 | end.uniq.each do |key|
360 | field key
361 | end
362 | end
363 |
364 | private :auto_assign_fields
365 |
366 | # Needed for ActiveRecord polymorphic associations
367 | def base_class
368 | ActiveHash::Base
369 | end
370 |
371 | # Needed for ActiveRecord polymorphic associations(rails/rails#32148)
372 | def polymorphic_name
373 | base_class.name
374 | end
375 |
376 | # Needed for ActiveRecord since rails/rails#47664
377 | def composite_primary_key?
378 | false
379 | end
380 |
381 | def reload
382 | reset_record_index
383 | self.data = _data
384 | mark_clean
385 | end
386 |
387 | private :reload
388 |
389 | def mark_dirty
390 | self.dirty = true
391 | end
392 |
393 | private :mark_dirty
394 |
395 | def mark_clean
396 | self.dirty = false
397 | end
398 |
399 | private :mark_clean
400 |
401 | def scope(name, body)
402 | raise ArgumentError, 'body needs to be callable' unless body.respond_to?(:call)
403 |
404 | self.scopes ||= {}
405 | self.scopes[name] = body
406 |
407 | the_meta_class.instance_eval do
408 | define_method(name) do |*args|
409 | instance_exec(*args, &body)
410 | end
411 | end
412 | end
413 |
414 | end
415 |
416 | def initialize(attributes = {})
417 | attributes.symbolize_keys!
418 | @attributes = attributes
419 | attributes.dup.each do |key, value|
420 | send "#{key}=", value
421 | end
422 | yield self if block_given?
423 | end
424 |
425 | def attributes
426 | if self.class.default_attributes
427 | (self.class.default_attributes.merge @attributes).freeze
428 | else
429 | @attributes
430 | end
431 | end
432 |
433 | def [](key)
434 | attributes[key]
435 | end
436 |
437 | def _read_attribute(key)
438 | attributes[key.to_sym]
439 | end
440 | alias_method :read_attribute, :_read_attribute
441 |
442 | def []=(key, val)
443 | @attributes[key] = val
444 | end
445 |
446 | def id
447 | attributes[:id] ? attributes[:id] : nil
448 | end
449 |
450 | def id=(id)
451 | @attributes[:id] = id
452 | end
453 |
454 | alias quoted_id id
455 |
456 | def new_record?
457 | !self.class.all.include?(self)
458 | end
459 |
460 | def destroyed?
461 | false
462 | end
463 |
464 | def persisted?
465 | self.class.all.map(&:id).include?(id)
466 | end
467 |
468 | def readonly?
469 | true
470 | end
471 |
472 | def eql?(other)
473 | other.instance_of?(self.class) and not id.nil? and (id == other.id)
474 | end
475 |
476 | alias == eql?
477 |
478 | def hash
479 | id.hash
480 | end
481 |
482 | def cache_key
483 | case
484 | when new_record?
485 | "#{self.class.cache_key}/new"
486 | when timestamp = self[:updated_at]
487 | if ActiveSupport::VERSION::MAJOR < 7
488 | "#{self.class.cache_key}/#{id}-#{timestamp.to_s(:number)}"
489 | else
490 | "#{self.class.cache_key}/#{id}-#{timestamp.to_fs(:number)}"
491 | end
492 | else
493 | "#{self.class.cache_key}/#{id}"
494 | end
495 | end
496 |
497 | def errors
498 | obj = Object.new
499 |
500 | def obj.[](key)
501 | []
502 | end
503 |
504 | def obj.full_messages()
505 | []
506 | end
507 |
508 | obj
509 | end
510 |
511 | def save(*args)
512 | unless self.class.exists?(self)
513 | self.class.insert(self)
514 | end
515 | true
516 | end
517 |
518 | alias save! save
519 |
520 | def valid?
521 | true
522 | end
523 |
524 | def marked_for_destruction?
525 | false
526 | end
527 |
528 | end
529 | end
530 |
--------------------------------------------------------------------------------
/lib/active_hash/condition.rb:
--------------------------------------------------------------------------------
1 | class ActiveHash::Relation::Condition
2 | attr_reader :constraints, :inverted
3 |
4 | def initialize(constraints)
5 | @constraints = constraints
6 | @inverted = false
7 | end
8 |
9 | def invert!
10 | @inverted = !inverted
11 |
12 | self
13 | end
14 |
15 | def matches?(record)
16 | match = begin
17 | return true unless constraints
18 |
19 | expectation_method = inverted ? :any? : :all?
20 |
21 | constraints.send(expectation_method) do |attribute, expected|
22 | value = record.read_attribute(attribute)
23 |
24 | matches_value?(value, expected)
25 | end
26 | end
27 |
28 | inverted ? !match : match
29 | end
30 |
31 | private
32 |
33 | def matches_value?(value, comparison)
34 | return comparison.any? { |v| matches_value?(value, v) } if comparison.is_a?(Array)
35 | return comparison.cover?(value) if comparison.is_a?(Range)
36 | return comparison.match?(value) if comparison.is_a?(Regexp)
37 |
38 | normalize(value) == normalize(comparison)
39 | end
40 |
41 | def normalize(value)
42 | value.respond_to?(:to_s) ? value&.to_s : value
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/active_hash/conditions.rb:
--------------------------------------------------------------------------------
1 | class ActiveHash::Relation::Conditions
2 | attr_reader :conditions
3 |
4 | delegate :<<, :map, to: :conditions
5 |
6 | def initialize(conditions = [])
7 | @conditions = conditions
8 | end
9 |
10 | def matches?(record)
11 | conditions.all? do |condition|
12 | condition.matches?(record)
13 | end
14 | end
15 |
16 | def self.wrap(conditions)
17 | return conditions if conditions.is_a?(self)
18 |
19 | new(conditions)
20 | end
21 | end
--------------------------------------------------------------------------------
/lib/active_hash/relation.rb:
--------------------------------------------------------------------------------
1 | module ActiveHash
2 | class Relation
3 | include Enumerable
4 |
5 | delegate :each, to: :records # Make Enumerable work
6 | delegate :equal?, :==, :===, :eql?, :sort!, to: :records
7 | delegate :empty?, :length, :first, :second, :third, :last, to: :records
8 | delegate :sample, to: :records
9 |
10 | attr_reader :conditions, :order_values, :klass, :all_records
11 |
12 | def initialize(klass, all_records, conditions = nil, order_values = nil)
13 | self.klass = klass
14 | self.all_records = all_records
15 | self.conditions = Conditions.wrap(conditions || [])
16 | self.order_values = order_values || []
17 | end
18 |
19 | def where(conditions_hash = :chain)
20 | return WhereChain.new(self) if conditions_hash == :chain
21 |
22 | spawn.where!(conditions_hash)
23 | end
24 |
25 | def pretty_print(pp)
26 | pp.pp(entries.to_ary)
27 | end
28 |
29 | class WhereChain
30 | attr_reader :relation
31 |
32 | def initialize(relation)
33 | @relation = relation
34 | end
35 |
36 | def not(conditions_hash)
37 | relation.conditions << Condition.new(conditions_hash).invert!
38 | relation
39 | end
40 | end
41 |
42 | def order(*options)
43 | spawn.order!(*options)
44 | end
45 |
46 | def reorder(*options)
47 | spawn.reorder!(*options)
48 | end
49 |
50 | def where!(conditions_hash, inverted = false)
51 | self.conditions << Condition.new(conditions_hash)
52 | self
53 | end
54 |
55 | def invert_where
56 | spawn.invert_where!
57 | end
58 |
59 | def invert_where!
60 | conditions.map(&:invert!)
61 | self
62 | end
63 |
64 | def spawn
65 | self.class.new(klass, all_records, conditions, order_values)
66 | end
67 |
68 | def order!(*options)
69 | check_if_method_has_arguments!(:order, options)
70 | self.order_values += preprocess_order_args(options)
71 | self
72 | end
73 |
74 | def reorder!(*options)
75 | check_if_method_has_arguments!(:order, options)
76 |
77 | self.order_values = preprocess_order_args(options)
78 | @records = apply_order_values(records, order_values)
79 |
80 | self
81 | end
82 |
83 | def records
84 | @records ||= begin
85 | filtered_records = apply_conditions(all_records, conditions)
86 | ordered_records = apply_order_values(filtered_records, order_values) # rubocop:disable Lint/UselessAssignment
87 | end
88 | end
89 |
90 | def reload
91 | @records = nil # Reset records
92 | self
93 | end
94 |
95 | def all(options = {})
96 | if options.key?(:conditions)
97 | where(options[:conditions])
98 | else
99 | where({})
100 | end
101 | end
102 |
103 | def find_by(options)
104 | where(options).first
105 | end
106 |
107 | def find_by!(options)
108 | find_by(options) || (raise RecordNotFound.new("Couldn't find #{klass.name}", klass.name))
109 | end
110 |
111 | def find(id = nil, *args, &block)
112 | case id
113 | when :all
114 | all
115 | when :first
116 | all(*args).first
117 | when Array
118 | id.map { |i| find(i) }
119 | when nil
120 | raise RecordNotFound.new("Couldn't find #{klass.name} without an ID", klass.name, "id") unless block_given?
121 | records.find(&block) # delegate to Enumerable#find if a block is given
122 | else
123 | find_by_id(id) || begin
124 | raise RecordNotFound.new("Couldn't find #{klass.name} with ID=#{id}", klass.name, "id", id)
125 | end
126 | end
127 | end
128 |
129 | def find_by_id(id)
130 | index = klass.send(:record_index)[id.to_s] # TODO: Make index in Base publicly readable instead of using send?
131 | return unless index
132 |
133 | record = all_records[index]
134 | record if conditions.matches?(record)
135 | end
136 |
137 | def count
138 | return super if block_given?
139 | length
140 | end
141 |
142 | def size
143 | length
144 | end
145 |
146 | def pluck(*column_names)
147 | if column_names.length == 1
148 | column_name = column_names.first
149 | all.map { |record| record.public_send(column_name) }
150 | else
151 | all.map { |record| column_names.map { |column_name| record.public_send(column_name) } }
152 | end
153 | end
154 |
155 | def ids
156 | pluck(:id)
157 | end
158 |
159 | def pick(*column_names)
160 | pluck(*column_names).first
161 | end
162 |
163 | def to_ary
164 | records.dup
165 | end
166 |
167 | def method_missing(method_name, *args)
168 | return super unless klass.scopes&.key?(method_name)
169 |
170 | instance_exec(*args, &klass.scopes[method_name])
171 | end
172 |
173 | def respond_to_missing?(method_name, include_private = false)
174 | klass.scopes&.key?(method_name) || super
175 | end
176 |
177 | private
178 |
179 | attr_writer :conditions, :order_values, :klass, :all_records
180 |
181 | def apply_conditions(records, conditions)
182 | return records if conditions.blank?
183 |
184 | records.select do |record|
185 | conditions.matches?(record)
186 | end
187 | end
188 |
189 | def check_if_method_has_arguments!(method_name, args)
190 | return unless args.blank?
191 |
192 | raise ArgumentError,
193 | "The method .#{method_name}() must contain arguments."
194 | end
195 |
196 | def preprocess_order_args(order_args)
197 | order_args.reject!(&:blank?)
198 | return order_args.reverse! unless order_args.first.is_a?(String)
199 |
200 | ary = order_args.first.split(', ')
201 | ary.map! { |e| e.split(/\W+/) }.reverse!
202 | end
203 |
204 | def apply_order_values(records, args)
205 | ordered_records = records.dup
206 |
207 | args.each do |arg|
208 | field, dir = if arg.is_a?(Hash)
209 | arg.to_a.flatten.map(&:to_sym)
210 | elsif arg.is_a?(Array)
211 | arg.map(&:to_sym)
212 | else
213 | arg.to_sym
214 | end
215 |
216 | ordered_records.sort! do |a, b|
217 | if dir.present? && dir.to_sym.upcase.equal?(:DESC)
218 | b[field] <=> a[field]
219 | else
220 | a[field] <=> b[field]
221 | end
222 | end
223 | end
224 |
225 | ordered_records
226 | end
227 | end
228 | end
229 |
--------------------------------------------------------------------------------
/lib/active_hash/version.rb:
--------------------------------------------------------------------------------
1 | module ActiveHash
2 | module Gem
3 | VERSION = "3.3.1"
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/lib/active_json/base.rb:
--------------------------------------------------------------------------------
1 | module ActiveJSON
2 | class Base < ActiveFile::Base
3 | extend ActiveFile::HashAndArrayFiles
4 | class << self
5 | def load_file
6 | if (data = raw_data).is_a?(Array)
7 | data
8 | elsif data.respond_to?(:values)
9 | data.values
10 | end
11 | end
12 |
13 | def extension
14 | "json"
15 | end
16 |
17 | private
18 | def load_path(path)
19 | JSON.load(File.open(path, 'r:bom|utf-8'))
20 | end
21 |
22 | end
23 | end
24 |
25 | end
26 |
--------------------------------------------------------------------------------
/lib/active_yaml/aliases.rb:
--------------------------------------------------------------------------------
1 | module ActiveYaml
2 |
3 | module Aliases
4 | def self.included(base)
5 | base.extend(ClassMethods)
6 | end
7 |
8 | ALIAS_KEY_REGEXP = /^\//.freeze
9 |
10 | module ClassMethods
11 |
12 | def insert(record)
13 | super if record.attributes.present?
14 | end
15 |
16 | def raw_data
17 | d = super
18 | if d.kind_of?(Array)
19 | d.reject do |h|
20 | h.keys.any? { |k| k.match(ALIAS_KEY_REGEXP) }
21 | end
22 | else
23 | d.reject do |k, v|
24 | v.kind_of?(Hash) && k.match(ALIAS_KEY_REGEXP)
25 | end
26 | end
27 | end
28 |
29 | end
30 | end
31 |
32 | end
33 |
--------------------------------------------------------------------------------
/lib/active_yaml/base.rb:
--------------------------------------------------------------------------------
1 | require 'yaml'
2 |
3 | module ActiveYaml
4 |
5 | class Base < ActiveFile::Base
6 | extend ActiveFile::HashAndArrayFiles
7 |
8 | cattr_accessor :process_erb, instance_accessor: false
9 | @@process_erb = true
10 |
11 | class << self
12 | def load_file
13 | if (data = raw_data).is_a?(Array)
14 | data
15 | elsif data.respond_to?(:values)
16 | data.map{ |key, value| {"key" => key}.merge(value) }
17 | end
18 | end
19 |
20 | def extension
21 | "yml"
22 | end
23 |
24 | private
25 | if Psych::VERSION >= "4.0.0"
26 | def load_path(path)
27 | result = File.read(path)
28 | result = ERB.new(result).result if process_erb
29 | YAML.unsafe_load(result)
30 | end
31 | else
32 | def load_path(path)
33 | result = File.read(path)
34 | result = ERB.new(result).result if process_erb
35 | YAML.load(result)
36 | end
37 | end
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/associations/associations.rb:
--------------------------------------------------------------------------------
1 | module ActiveHash
2 | module Associations
3 |
4 | module ActiveRecordExtensions
5 | def self.extended(base)
6 | require_relative 'reflection_extensions'
7 | end
8 |
9 | def has_many(association_id, scope = nil, **options, &extension)
10 | if options[:through]
11 | klass_name = association_id.to_s.classify
12 | klass = klass_name.safe_constantize
13 |
14 | if klass && klass < ActiveHash::Base
15 | define_method(association_id) do
16 | join_models = send(options[:through])
17 | join_models.flat_map do |join_model|
18 | join_model.send(association_id.to_s.singularize)
19 | end.uniq
20 | end
21 |
22 | return
23 | end
24 | end
25 |
26 | super
27 | end
28 |
29 | def belongs_to(name, scope = nil, **options)
30 | klass_name = options.key?(:class_name) ? options[:class_name] : name.to_s.camelize
31 | klass = klass_name.safe_constantize
32 |
33 | if klass && klass < ActiveHash::Base
34 | options = { class_name: klass_name }.merge(options)
35 | belongs_to_active_hash(name, options)
36 | else
37 | super
38 | end
39 | end
40 |
41 | def belongs_to_active_hash(association_id, options = {})
42 | options = {
43 | :class_name => association_id.to_s.camelize,
44 | :foreign_key => association_id.to_s.foreign_key,
45 | :shortcuts => []
46 | }.merge(options)
47 | # Define default primary_key with provided class_name if any
48 | options[:primary_key] ||= options[:class_name].safe_constantize.primary_key
49 | options[:shortcuts] = [options[:shortcuts]] unless options[:shortcuts].kind_of?(Array)
50 |
51 | define_method(association_id) do
52 | options[:class_name].safe_constantize.send("find_by_#{options[:primary_key]}", send(options[:foreign_key]))
53 | end
54 |
55 | define_method("#{association_id}=") do |new_value|
56 | send "#{options[:foreign_key]}=", new_value ? new_value.send(options[:primary_key]) : nil
57 | end
58 |
59 | options[:shortcuts].each do |shortcut|
60 | define_method("#{association_id}_#{shortcut}") do
61 | send(association_id).try(shortcut)
62 | end
63 |
64 | define_method("#{association_id}_#{shortcut}=") do |new_value|
65 | send "#{association_id}=", new_value ? options[:class_name].safe_constantize.send("find_by_#{shortcut}", new_value) : nil
66 | end
67 | end
68 |
69 | if ActiveRecord::Reflection.respond_to?(:create)
70 | if defined?(ActiveHash::Reflection::BelongsToReflection)
71 | reflection = ActiveHash::Reflection::BelongsToReflection.new(association_id.to_sym, nil, options, self)
72 | else
73 | reflection = ActiveRecord::Reflection.create(
74 | :belongs_to,
75 | association_id.to_sym,
76 | nil,
77 | options,
78 | self
79 | )
80 | end
81 |
82 | ActiveRecord::Reflection.add_reflection(
83 | self,
84 | association_id.to_sym,
85 | reflection
86 | )
87 | else
88 | method = ActiveRecord::Base.method(:create_reflection)
89 | if method.respond_to?(:parameters) && method.parameters.length == 5
90 | create_reflection(
91 | :belongs_to,
92 | association_id.to_sym,
93 | nil,
94 | options,
95 | self
96 | )
97 | else
98 | create_reflection(
99 | :belongs_to,
100 | association_id.to_sym,
101 | options,
102 | options[:class_name].safe_constantize
103 | )
104 | end
105 | end
106 | end
107 | end
108 |
109 | def self.included(base)
110 | base.extend Methods
111 | end
112 |
113 | module Methods
114 | def has_many(association_id, options = {})
115 | define_method(association_id) do
116 | options = {
117 | :class_name => association_id.to_s.classify,
118 | :foreign_key => self.class.to_s.foreign_key,
119 | :primary_key => self.class.primary_key
120 | }.merge(options)
121 |
122 | klass = options[:class_name].safe_constantize
123 | primary_key_value = send(options[:primary_key])
124 | foreign_key = options[:foreign_key].to_sym
125 |
126 | if Object.const_defined?(:ActiveRecord) && ActiveRecord.const_defined?(:Relation) && klass < ActiveRecord::Relation
127 | klass.where(foreign_key => primary_key_value)
128 | elsif klass.respond_to?(:scoped)
129 | klass.scoped(:conditions => {foreign_key => primary_key_value})
130 | else
131 | klass.where(foreign_key => primary_key_value)
132 | end
133 | end
134 |
135 | define_method("#{association_id.to_s.underscore.singularize}_ids") do
136 | public_send(association_id).map(&:id)
137 | end
138 | end
139 |
140 | def has_one(association_id, options = {})
141 | define_method(association_id) do
142 | options = {
143 | :class_name => association_id.to_s.classify,
144 | :foreign_key => self.class.to_s.foreign_key,
145 | :primary_key => self.class.primary_key
146 | }.merge(options)
147 |
148 | scope = options[:class_name].safe_constantize
149 |
150 | if scope.respond_to?(:scoped) && options[:conditions]
151 | scope = scope.scoped(:conditions => options[:conditions])
152 | end
153 | scope.send("find_by_#{options[:foreign_key]}", send(options[:primary_key]))
154 | end
155 | end
156 |
157 | def belongs_to(association_id, options = {})
158 | options = {
159 | :class_name => association_id.to_s.classify,
160 | :foreign_key => association_id.to_s.foreign_key,
161 | :primary_key => "id"
162 | }.merge(options)
163 |
164 | field options[:foreign_key].to_sym
165 |
166 | define_method(association_id) do
167 | options[:class_name].safe_constantize.send("find_by_#{options[:primary_key]}", send(options[:foreign_key]))
168 | end
169 |
170 | define_method("#{association_id}=") do |new_value|
171 | attributes[options[:foreign_key].to_sym] = new_value ? new_value.send(options[:primary_key]) : nil
172 | end
173 | end
174 | end
175 |
176 | end
177 | end
178 |
--------------------------------------------------------------------------------
/lib/associations/reflection_extensions.rb:
--------------------------------------------------------------------------------
1 | module ActiveHash
2 | module Reflection
3 | class BelongsToReflection < ActiveRecord::Reflection::BelongsToReflection
4 | def compute_class(name)
5 | if polymorphic?
6 | raise ArgumentError, "Polymorphic associations do not support computing the class."
7 | end
8 |
9 | begin
10 | klass = active_record.send(:compute_type, name)
11 | rescue NameError => error
12 | if error.name.match?(/(?:\A|::)#{name}\z/)
13 | message = "Missing model class #{name} for the #{active_record}##{self.name} association."
14 | message += " You can specify a different model class with the :class_name option." unless options[:class_name]
15 | raise NameError.new(message, name)
16 | else
17 | raise
18 | end
19 | end
20 |
21 | klass
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/enum/enum.rb:
--------------------------------------------------------------------------------
1 | module ActiveHash
2 | module Enum
3 |
4 | DuplicateEnumAccessor = Class.new(RuntimeError)
5 |
6 | def self.included(base)
7 | base.extend(Methods)
8 | end
9 |
10 | module Methods
11 |
12 | def enum_accessor(*field_names)
13 | @enum_accessors = field_names
14 | reload
15 | end
16 |
17 | def enum(columns)
18 | columns.each do |column, values|
19 | values = values.zip(values.map(&:to_s)).to_h if values.is_a?(Array)
20 | values.each do |method, value|
21 | class_eval <<~METHOD, __FILE__, __LINE__ + 1
22 | # frozen_string_literal: true
23 | def #{method}?
24 | #{column} == #{value.inspect}
25 | end
26 | METHOD
27 | end
28 | end
29 | end
30 |
31 | def insert(record)
32 | super
33 | set_constant(record) if defined?(@enum_accessors)
34 | end
35 |
36 | def delete_all
37 | if @enum_accessors.present?
38 | @records.each do |record|
39 | constant = constant_for(record, @enum_accessors)
40 | remove_const(constant) if const_defined?(constant, false)
41 | end
42 | end
43 | super
44 | end
45 |
46 | def set_constant(record)
47 | constant = constant_for(record, @enum_accessors)
48 | return nil if constant.blank?
49 |
50 | unless const_defined?(constant, false)
51 | const_set(constant, record)
52 | else
53 | raise DuplicateEnumAccessor, "#{constant} already defined for #{self.class}" unless const_get(constant, false) == record
54 | end
55 | end
56 |
57 | private :set_constant
58 |
59 | def constant_for(record, field_names)
60 | field_value = field_names.map { |name| record.attributes[name] }.join("_")
61 | if constant = !field_value.nil? && field_value.dup
62 | constant.gsub!(/\W+/, "_")
63 | constant.gsub!(/^_|_$/, '')
64 | constant.upcase!
65 | constant
66 | end
67 | end
68 |
69 | private :constant_for
70 | end
71 |
72 | end
73 |
74 | end
75 |
--------------------------------------------------------------------------------
/spec/active_file/base_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ActiveFile::Base do
4 | before do
5 | class Country < ActiveFile::Base
6 | end
7 | end
8 |
9 | after do
10 | Object.send :remove_const, :Country
11 | end
12 |
13 | describe ".multiple_files?" do
14 | it "is false" do
15 | expect(Country.multiple_files?).to be_falsey
16 | end
17 | end
18 |
19 | describe ".filename=" do
20 | before do
21 | Country.filename = "foo-izzle"
22 |
23 | class Bar < ActiveFile::Base
24 | self.filename = "bar-izzle"
25 | end
26 | end
27 | after { Object.send :remove_const, :Bar }
28 |
29 | it "sets the filename on a per-subclass basis" do
30 | expect(Country.filename).to eq("foo-izzle")
31 | expect(Bar.filename).to eq("bar-izzle")
32 | end
33 | end
34 |
35 | describe ".set_filename" do
36 | before do
37 | Country.set_filename "foo-izzle"
38 |
39 | class Bar < ActiveFile::Base
40 | set_filename "bar-izzle"
41 | end
42 | end
43 | after { Object.send :remove_const, :Bar }
44 |
45 | it "sets the filename on a per-subclass basis" do
46 | expect(Country.filename).to eq("foo-izzle")
47 | expect(Bar.filename).to eq("bar-izzle")
48 | end
49 | end
50 |
51 | describe ".root_path=" do
52 | before do
53 | Country.root_path = "foo-izzle"
54 |
55 | class Bar < ActiveFile::Base
56 | self.root_path = "bar-izzle"
57 | end
58 | end
59 | after { Object.send :remove_const, :Bar }
60 |
61 | it "sets the root_path on a per-subclass basis" do
62 | expect(Country.root_path).to eq("foo-izzle")
63 | expect(Bar.root_path).to eq("bar-izzle")
64 | end
65 | end
66 |
67 | describe ".set_root_path" do
68 | before do
69 | Country.set_root_path "foo-izzle"
70 |
71 | class Bar < ActiveFile::Base
72 | set_root_path "bar-izzle"
73 | end
74 | end
75 | after { Object.send :remove_const, :Bar }
76 |
77 | it "sets the root_path on a per-subclass basis" do
78 | expect(Country.root_path).to eq("foo-izzle")
79 | expect(Bar.root_path).to eq("bar-izzle")
80 | end
81 | end
82 |
83 | describe ".full_path" do
84 | it "defaults to the directory of the calling file" do
85 | class Country
86 | def self.extension() "foo" end
87 | end
88 |
89 | expect(Country.full_path).to eq("#{Dir.pwd}/countries.foo")
90 | end
91 | end
92 |
93 | describe ".reload" do
94 | before do
95 | class Country
96 | def self.load_file()
97 | {"new_york"=>{"name"=>"New York", "id"=>1}}.values
98 | end
99 | end
100 | Country.reload # initial load
101 | end
102 |
103 | context "when nothing has been modified" do
104 | it "does not reload anything" do
105 | class Country
106 | def self.load_file()
107 | raise "should not have been called"
108 | end
109 | end
110 | expect(Country.dirty).to be_falsey
111 | Country.reload
112 | expect(Country.dirty).to be_falsey
113 | end
114 | end
115 |
116 | context "when forced" do
117 | it "reloads the data" do
118 | class Country
119 | def self.load_file()
120 | {"new_york"=>{"name"=>"New York", "id"=>2}}.values
121 | end
122 | end
123 | expect(Country.dirty).to be_falsey
124 | expect(Country.find_by_id(2)).to be_nil
125 | Country.reload(true)
126 | expect(Country.dirty).to be_falsey
127 | expect(Country.find(2).name).to eq("New York")
128 | end
129 | end
130 |
131 | context "when the data has been modified" do
132 | it "reloads the data" do
133 | Country.create!
134 | expect(Country.dirty).to be_truthy
135 | Country.reload
136 | expect(Country.dirty).to be_falsey
137 | end
138 | end
139 | end
140 |
141 | end
142 |
--------------------------------------------------------------------------------
/spec/active_file/multiple_files_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ActiveFile::MultipleFiles do
4 | before do
5 | class Country < ActiveFile::Base
6 | use_multiple_files
7 | end
8 | end
9 |
10 | after do
11 | Object.send :remove_const, :Country
12 | end
13 |
14 | describe ".filenames=" do
15 | before do
16 | Country.filenames = ["country-file"]
17 |
18 | class Bar < ActiveFile::Base
19 | use_multiple_files
20 | self.filenames = ["bar-file"]
21 | end
22 | end
23 | after { Object.send :remove_const, :Bar }
24 |
25 | it "sets the filenames on a per-subclass basis" do
26 | expect(Country.filenames).to eq(["country-file"])
27 | expect(Bar.filenames).to eq(["bar-file"])
28 | end
29 | end
30 |
31 | describe "set_filenames" do
32 | before do
33 | Country.set_filenames "country-file"
34 |
35 | class Bar < ActiveFile::Base
36 | use_multiple_files
37 | set_filenames "bar-file", "baz-file"
38 | end
39 | end
40 | after { Object.send :remove_const, :Bar }
41 |
42 | it "sets the filenames on a per-subclass basis" do
43 | expect(Country.filenames).to eq(["country-file"])
44 | expect(Bar.filenames).to eq(["bar-file", "baz-file"])
45 | end
46 | end
47 |
48 | describe ".multiple_files?" do
49 | it "is true" do
50 | expect(Country.multiple_files?).to be_truthy
51 | end
52 |
53 | context "on a per class basis" do
54 | before do
55 | class Bar < ActiveFile::Base
56 | end
57 | end
58 | after { Object.send :remove_const, :Bar }
59 |
60 | it "is true for classes with filenames" do
61 | expect(Country.multiple_files?).to be_truthy
62 | expect(Bar.multiple_files?).to be_falsey
63 | end
64 | end
65 | end
66 |
67 | describe ".full_paths" do
68 | it "defaults to the directory of the calling file" do
69 | class Country
70 | def self.extension() "foo" end
71 | end
72 |
73 | expect(Country.full_paths).to eq(["#{Dir.pwd}/countries.foo"])
74 | end
75 |
76 | context "given multiple files do" do
77 | it "is good" do
78 | class Country
79 | def self.extension() "foo" end
80 | self.filenames = ["fizz", "bazz"]
81 | end
82 |
83 | expect(Country.full_paths).to eq(["#{Dir.pwd}/fizz.foo", "#{Dir.pwd}/bazz.foo"])
84 | end
85 | end
86 | end
87 | end
88 |
--------------------------------------------------------------------------------
/spec/active_hash/base_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ActiveHash, "Base" do
4 |
5 | before do
6 | class Country < ActiveHash::Base
7 | end
8 | end
9 |
10 | after do
11 | Object.send :remove_const, :Country
12 | end
13 |
14 | it "passes LocalJumpError through in .transaction when no block is given" do
15 | expect { Country.transaction }.to raise_error(LocalJumpError)
16 | end
17 |
18 | describe ".new" do
19 | it "yields a block" do
20 | expect { |b| Country.new(&b) }.to yield_with_args(Country)
21 | end
22 |
23 | context "initializing with a block" do
24 | subject do
25 | Country.fields :name
26 | Country.new do |country|
27 | country.name = 'Germany'
28 | end
29 | end
30 |
31 | it "sets assigns the fields" do
32 | expect(subject.name).to eq('Germany')
33 | end
34 | end
35 | end
36 |
37 | describe ".fields" do
38 | before do
39 | Country.fields :name, :iso_name
40 | end
41 |
42 | it "defines a reader for each field" do
43 | expect(Country.new).to respond_to(:name)
44 | expect(Country.new).to respond_to(:iso_name)
45 | end
46 |
47 | it "defines interrogator methods for each field" do
48 | expect(Country.new).to respond_to(:name?)
49 | expect(Country.new).to respond_to(:iso_name?)
50 | end
51 |
52 | it "defines single finder methods for each field" do
53 | expect(Country).to respond_to(:find_by_name)
54 | expect(Country).to respond_to(:find_by_iso_name)
55 | end
56 |
57 | it "defines banged single finder methods for each field" do
58 | expect(Country).to respond_to(:find_by_name!)
59 | expect(Country).to respond_to(:find_by_iso_name!)
60 | end
61 |
62 | it "defines array finder methods for each field" do
63 | expect(Country).to respond_to(:find_all_by_name)
64 | expect(Country).to respond_to(:find_all_by_iso_name)
65 | end
66 |
67 | it "does not define banged array finder methods for each field" do
68 | expect(Country).not_to respond_to(:find_all_by_name!)
69 | expect(Country).not_to respond_to(:find_all_by_iso_name!)
70 | end
71 |
72 | it "defines single finder methods for all combinations of fields" do
73 | expect(Country).to respond_to(:find_by_name_and_iso_name)
74 | expect(Country).to respond_to(:find_by_iso_name_and_name)
75 | end
76 |
77 | it "defines banged single finder methods for all combinations of fields" do
78 | expect(Country).to respond_to(:find_by_name_and_iso_name!)
79 | expect(Country).to respond_to(:find_by_iso_name_and_name!)
80 | end
81 |
82 | it "defines array finder methods for all combinations of fields" do
83 | expect(Country).to respond_to(:find_all_by_name_and_iso_name)
84 | expect(Country).to respond_to(:find_all_by_iso_name_and_name)
85 | end
86 |
87 | it "does not define banged array finder methods for all combinations of fields" do
88 | expect(Country).not_to respond_to(:find_all_by_name_and_iso_name!)
89 | expect(Country).not_to respond_to(:find_all_by_iso_name_and_name!)
90 | end
91 |
92 | it "allows you to pass options to the built-in find_by_* methods (but ignores the hash for now)" do
93 | expect(Country.find_by_name("Canada", :select => nil)).to be_nil
94 | expect(Country.find_all_by_name("Canada", :select => nil)).to eq([])
95 | end
96 |
97 | it "allows you to pass options to the custom find_by_* methods (but ignores the hash for now)" do
98 | expect(Country.find_by_name_and_iso_name("Canada", "CA", :select => nil)).to be_nil
99 | expect(Country.find_all_by_name_and_iso_name("Canada", "CA", :select => nil)).to eq([])
100 | end
101 |
102 | it "blows up if you try to overwrite :attributes" do
103 | expect do
104 | Country.field :attributes
105 | end.to raise_error(ActiveHash::ReservedFieldError)
106 | end
107 | end
108 |
109 | describe ".field_names" do
110 | before do
111 | Country.fields :name, :iso_name, "size"
112 | end
113 |
114 | it "returns an array of field names" do
115 | expect(Country.field_names).to eq([:name, :iso_name, :size])
116 | end
117 | end
118 |
119 | describe ".column_names" do
120 | before do
121 | Country.fields :name, :iso_name, "size"
122 | end
123 |
124 | it "returns an array of column names" do
125 | skip "Not supported in Ruby 3.0.0" if RUBY_VERSION < "3.0.0"
126 | expect(Country.column_names).to eq(["name", "iso_name", "size"])
127 | end
128 | end
129 |
130 | describe ".data=" do
131 | before do
132 | class Region < ActiveHash::Base
133 | field :description
134 | end
135 | end
136 |
137 | it "populates the object with data and auto-assigns keys" do
138 | Country.data = [{:name => "US"}, {:name => "Canada"}]
139 | expect(Country.data).to eq([{:name => "US", :id => 1}, {:name => "Canada", :id => 2}])
140 | end
141 |
142 | it "allows each of it's subclasses to have it's own data" do
143 | Country.data = [{:name => "US"}, {:name => "Canada"}]
144 | Region.data = [{:description => "A big region"}, {:description => "A remote region"}]
145 |
146 | expect(Country.data).to eq([{:name => "US", :id => 1}, {:name => "Canada", :id => 2}])
147 | expect(Region.data).to eq([{:description => "A big region", :id => 1}, {:description => "A remote region", :id => 2}])
148 | end
149 |
150 | it "marks the class as dirty" do
151 | expect(Country.dirty).to be_falsey
152 | Country.data = []
153 | expect(Country.dirty).to be_truthy
154 | end
155 | end
156 |
157 | describe ".add" do
158 | before do
159 | Country.fields :name
160 | end
161 |
162 | it "adds a record" do
163 | expect {
164 | Country.add :name => "Russia"
165 | }.to change { Country.count }
166 | end
167 |
168 | it "marks the class as dirty" do
169 | expect(Country.dirty).to be_falsey
170 | Country.add :name => "Russia"
171 | expect(Country.dirty).to be_truthy
172 | end
173 |
174 | it "returns the record" do
175 | record = Country.add :name => "Russia"
176 | expect(record.name).to eq("Russia")
177 | end
178 |
179 | it "should populate the id" do
180 | record = Country.add :name => "Russia"
181 | expect(record.id).not_to be_nil
182 | end
183 | end
184 |
185 | describe ".all" do
186 | before do
187 | Country.field :name
188 | Country.data = [
189 | {:id => 1, :name => "US"},
190 | {:id => 2, :name => "Canada"}
191 | ]
192 | end
193 |
194 | it "returns an empty array if data is nil" do
195 | Country.data = nil
196 | expect(Country.all).to be_empty
197 | end
198 |
199 | it "returns all data as inflated objects" do
200 | Country.all.all? { |country| expect(country).to be_kind_of(Country) }
201 | end
202 |
203 | it "populates the data correctly" do
204 | records = Country.all
205 | expect(records.first.id).to eq(1)
206 | expect(records.first.name).to eq("US")
207 | expect(records.last.id).to eq(2)
208 | expect(records.last.name).to eq("Canada")
209 | end
210 |
211 | it "re-populates the records after data= is called" do
212 | Country.data = [
213 | {:id => 45, :name => "Canada"}
214 | ]
215 | records = Country.all
216 | expect(records.first.id).to eq(45)
217 | expect(records.first.name).to eq("Canada")
218 | expect(records.length).to eq(1)
219 | end
220 |
221 | it "filters the records from a AR-like conditions hash" do
222 | record = Country.all(:conditions => {:name => 'US'})
223 | expect(record.count).to eq(1)
224 | expect(record.first.id).to eq(1)
225 | expect(record.first.name).to eq('US')
226 | end
227 | end
228 |
229 | describe ".reload" do
230 | before do
231 | Country.field :name
232 | Country.field :language
233 | Country.data = [
234 | {:id => 1, :name => "US", :language => 'English'},
235 | {:id => 2, :name => "Canada", :language => 'English'},
236 | {:id => 3, :name => "Mexico", :language => 'Spanish'}
237 | ]
238 | end
239 |
240 | it "it reloads cached records" do
241 | countries = Country.where(language: 'Spanish')
242 | expect(countries.count).to eq(1)
243 |
244 | Country.create(id: 4, name: 'Spain', language: 'Spanish')
245 |
246 | expect(countries.count).to eq(1)
247 | countries.reload
248 | expect(countries.count).to eq(2)
249 | end
250 | end
251 |
252 | describe ".where" do
253 | before do
254 | Country.field :name
255 | Country.field :language
256 | Country.data = [
257 | {:id => 1, :name => "US", :language => 'English'},
258 | {:id => 2, :name => "Canada", :language => 'English'},
259 | {:id => 3, :name => "Mexico", :language => 'Spanish'}
260 | ]
261 | end
262 |
263 | it 'returns a Relation class if conditions are provided' do
264 | expect(Country.where(language: 'English').class).to eq(ActiveHash::Relation)
265 | end
266 |
267 | it "returns WhereChain class if no conditions are provided" do
268 | expect(Country.where.class).to eq(ActiveHash::Relation::WhereChain)
269 | end
270 |
271 | it "returns all records when passed nil" do
272 | expect(Country.where(nil)).to eq(Country.all)
273 | end
274 |
275 | it "returns all records when an empty hash" do
276 | expect(Country.where({})).to eq(Country.all)
277 | end
278 |
279 | it "returns all data as inflated objects" do
280 | Country.where(:language => 'English').all? { |country| expect(country).to be_kind_of(Country) }
281 | end
282 |
283 | it "populates the data correctly" do
284 | records = Country.where(:language => 'English')
285 | expect(records.first.id).to eq(1)
286 | expect(records.first.name).to eq("US")
287 | expect(records.last.id).to eq(2)
288 | expect(records.last.name).to eq("Canada")
289 | end
290 |
291 | it "re-populates the records after data= is called" do
292 | Country.data = [
293 | {:id => 45, :name => "Canada"}
294 | ]
295 | records = Country.where(:name => 'Canada')
296 | expect(records.first.id).to eq(45)
297 | expect(records.first.name).to eq("Canada")
298 | expect(records.length).to eq(1)
299 | end
300 |
301 | it "filters the records from a AR-like conditions hash" do
302 | record = Country.where(:name => 'US')
303 | expect(record.count).to eq(1)
304 | expect(record.first.id).to eq(1)
305 | expect(record.first.name).to eq('US')
306 | end
307 |
308 | it "filters records when passed a hash with string keys" do
309 | record = Country.where('name' => 'US')
310 | expect(record.count).to eq(1)
311 | expect(record.first.id).to eq(1)
312 | expect(record.first.name).to eq('US')
313 | end
314 |
315 | it "raises an error if ids aren't unique" do
316 | expect do
317 | Country.data = [
318 | {:id => 1, :name => "US", :language => 'English'},
319 | {:id => 2, :name => "Canada", :language => 'English'},
320 | {:id => 2, :name => "Mexico", :language => 'Spanish'}
321 | ]
322 | end.to raise_error(ActiveHash::IdError)
323 | end
324 |
325 | it "returns a record for specified id" do
326 | record = Country.where(id: 1)
327 | expect(record.first.id).to eq(1)
328 | expect(record.first.name).to eq('US')
329 | end
330 |
331 | it "returns empty array" do
332 | expect(Country.where(id: nil)).to eq []
333 | end
334 |
335 | it "returns multiple records for multiple ids" do
336 | expect(Country.where(:id => %w(1 2)).map(&:id)).to match_array([1,2])
337 | end
338 |
339 | it "returns multiple records for range argument" do
340 | expect(Country.where(:id => 1..2).map(&:id)).to match_array([1,2])
341 | end
342 |
343 | if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.6.0")
344 | it "returns multiple records for infinite range argument" do
345 | expect(Country.where(:id => eval("2..")).map(&:id)).to match_array([2,3])
346 | end
347 | end
348 |
349 | it "filters records for multiple values" do
350 | expect(Country.where(:name => %w(US Canada)).map(&:name)).to match_array(%w(US Canada))
351 | end
352 |
353 | it "filters records by a RegEx" do
354 | expect(Country.where(:language => /Eng/).map(&:name)).to match_array(%w(US Canada))
355 | end
356 |
357 | it "filters records for multiple symbol values" do
358 | expect(Country.where(:name => [:US, :Canada]).map(&:name)).to match_array(%w(US Canada))
359 | end
360 |
361 | it 'is chainable' do
362 | where_relation = Country.where(language: 'English')
363 |
364 | expect(where_relation.length).to eq 2
365 | expect(where_relation.map(&:id)).to eq([1, 2])
366 | chained_where_relation = where_relation.where(name: 'US')
367 | expect(chained_where_relation.length).to eq 1
368 | expect(chained_where_relation.map(&:id)).to eq([1])
369 | end
370 | end
371 |
372 | describe ".invert_where" do
373 | before do
374 | Country.field :name
375 | Country.field :language
376 | Country.data = [
377 | {:id => 1, :name => "US", :language => 'English'},
378 | {:id => 2, :name => "Canada", :language => 'English'},
379 | {:id => 3, :name => "Mexico", :language => 'Spanish'}
380 | ]
381 | end
382 |
383 | it "inverts all conditions" do
384 | expect(Country.where(id: 1).where.not(id: 3).invert_where.map(&:name)).to match_array(%w(Mexico))
385 | end
386 | end
387 |
388 | describe ".where.not" do
389 | before do
390 | Country.field :name
391 | Country.field :language
392 | Country.data = [
393 | {:id => 1, :name => "US", :language => 'English'},
394 | {:id => 2, :name => "Canada", :language => 'English'},
395 | {:id => 3, :name => "Mexico", :language => 'Spanish'}
396 | ]
397 | end
398 |
399 | it "raises ArgumentError if no conditions are provided" do
400 | expect{
401 | Country.where.not
402 | }.to raise_error(ArgumentError)
403 | end
404 |
405 | it 'returns a chainable Relation when conditions are passed' do
406 | expect(Country.where.not(language: 'Spanish').class).to eq(ActiveHash::Relation)
407 | end
408 |
409 | it "returns all records when passed nil" do
410 | expect(Country.where.not(nil)).to eq(Country.all)
411 | end
412 |
413 | it "returns all records when an empty hash" do
414 | expect(Country.where.not({})).to eq(Country.all)
415 | end
416 |
417 | it "returns all records as inflated objects" do
418 | Country.where.not(:language => 'English').all? { |country| expect(country).to be_kind_of(Country) }
419 | end
420 |
421 | it "populates the records correctly" do
422 | records = Country.where.not(:language => 'Spanish')
423 | expect(records.first.id).to eq(1)
424 | expect(records.first.name).to eq("US")
425 | expect(records.last.id).to eq(2)
426 | expect(records.last.name).to eq("Canada")
427 | expect(records.length).to eq(2)
428 | end
429 |
430 | it "re-populates the records after data= is called" do
431 | Country.data = [
432 | {:id => 45, :name => "Canada"}
433 | ]
434 | records = Country.where.not(:name => "US")
435 | expect(records.first.id).to eq(45)
436 | expect(records.first.name).to eq("Canada")
437 | expect(records.length).to eq(1)
438 | end
439 |
440 | it "filters the records from a AR-like conditions hash" do
441 | record = Country.where.not(:name => 'US')
442 | expect(record.first.id).to eq(2)
443 | expect(record.first.name).to eq('Canada')
444 | expect(record.last.id).to eq(3)
445 | expect(record.last.name).to eq('Mexico')
446 | expect(record.length).to eq(2)
447 | end
448 |
449 | it "returns the records for NOT specified id" do
450 | record = Country.where.not(id: 1)
451 | expect(record.first.id).to eq(2)
452 | expect(record.first.name).to eq('Canada')
453 | expect(record.last.id).to eq(3)
454 | expect(record.last.name).to eq('Mexico')
455 | end
456 |
457 | it "returns a chainable relation even if id is given" do
458 | expect(Country.where.not(id: 1).class).to eq(ActiveHash::Relation)
459 | end
460 |
461 | it "returns all records when id is nil" do
462 | expect(Country.where.not(:id => nil)).to eq Country.all
463 | end
464 |
465 | it "filters records for multiple ids" do
466 | expect(Country.where.not(:id => [1, 2]).pluck(:id)).to match_array([3])
467 | end
468 |
469 | it "filters records for multiple values" do
470 | expect(Country.where.not(:name => %w[US Canada]).pluck(:name)).to match_array(%w[Mexico])
471 | end
472 |
473 | it "filters records for multiple symbol values" do
474 | expect(Country.where.not(:name => %i[US Canada]).pluck(:name)).to match_array(%w[Mexico])
475 | end
476 |
477 | it "filters records for multiple conditions" do
478 | expect(Country.where.not(:id => 1, :name => 'Mexico')).to match_array([Country.find(2)])
479 | end
480 | end
481 |
482 | describe ".find_by" do
483 | before do
484 | Country.field :name
485 | Country.field :language
486 | Country.data = [
487 | {:id => 1, :name => "US", :language => 'English'},
488 | {:id => 2, :name => "Canada", :language => 'English'},
489 | {:id => 3, :name => "Mexico", :language => 'Spanish'},
490 | {:id => 5, :name => "Any", :language => nil}
491 | ]
492 | end
493 |
494 | it "raises ArgumentError if no conditions are provided" do
495 | expect{
496 | Country.find_by
497 | }.to raise_error(ArgumentError)
498 | end
499 |
500 | it "returns first record when passed nil" do
501 | expect(Country.find_by(nil)).to eq(Country.first)
502 | end
503 |
504 | it "returns all data as inflated objects" do
505 | expect(Country.find_by(:language => 'English')).to be_kind_of(Country)
506 | end
507 |
508 | it "populates the data correctly" do
509 | record = Country.find_by(:language => 'English')
510 | expect(record.id).to eq(1)
511 | expect(record.name).to eq("US")
512 | end
513 |
514 | it "re-populates the records after data= is called" do
515 | Country.data = [
516 | {:id => 45, :name => "Canada"}
517 | ]
518 | record = Country.find_by(:name => 'Canada')
519 | expect(record.id).to eq(45)
520 | expect(record.name).to eq("Canada")
521 | end
522 |
523 | it "filters the records from a AR-like conditions hash" do
524 | record = Country.find_by(:name => 'US')
525 | expect(record.id).to eq(1)
526 | expect(record.name).to eq('US')
527 | end
528 |
529 | it "finds the record with the specified id as a string" do
530 | record = Country.find_by(:id => '1')
531 | expect(record.name).to eq('US')
532 | end
533 |
534 | it "returns the record that matches options" do
535 | expect(Country.find_by(:name => "US").id).to eq(1)
536 | end
537 |
538 | it "returns the record that matches options with symbol value" do
539 | expect(Country.find_by(:name => :US).id).to eq(1)
540 | end
541 |
542 | it "returns nil when not matched in candidates" do
543 | expect(Country.find_by(:name => "UK")).to be_nil
544 | end
545 |
546 | it "returns nil when passed a wrong id" do
547 | expect(Country.find_by(:id => 4)).to be_nil
548 | end
549 |
550 | it "finds record by nil value" do
551 | expect(Country.find_by(:language => nil).id).to eq(5)
552 | end
553 |
554 | it "doesn't finds nil records when searching for ''" do
555 | expect(Country.find_by(:language => '')).to be_nil
556 | end
557 | end
558 |
559 | describe ".find_by!" do
560 | before do
561 | Country.field :name
562 | Country.field :language
563 | Country.data = [
564 | {:id => 1, :name => "US", :language => 'English'}
565 | ]
566 | end
567 |
568 | subject { Country.find_by!(name: word) }
569 |
570 | context 'when data exists' do
571 | let(:word) { 'US' }
572 | it { expect(subject.id).to eq 1 }
573 | end
574 |
575 | context 'when data not found' do
576 | let(:word) { 'UK' }
577 | it { expect{ subject }.to raise_error ActiveHash::RecordNotFound }
578 | it "raises 'RecordNotFound' when passed a wrong id" do
579 | expect { Country.find_by!(id: 2) }.
580 | to raise_error ActiveHash::RecordNotFound
581 | end
582 |
583 | it "raises 'RecordNotFound' when passed wrong id and options" do
584 | expect { Country.find_by!(id: 2, name: "FR") }.
585 | to raise_error ActiveHash::RecordNotFound
586 | end
587 | end
588 | end
589 |
590 | describe ".count" do
591 | before do
592 | Country.data = [
593 | {:id => 1, :name => "US"},
594 | {:id => 2, :name => "Canada"}
595 | ]
596 | end
597 |
598 | it "returns the number of elements in the array" do
599 | expect(Country.count).to eq(2)
600 | end
601 | end
602 |
603 | describe ".pluck" do
604 | before do
605 | Country.data = [
606 | {:id => 1, :name => "US", :language => "English"},
607 | {:id => 2, :name => "Canada", :language => "English"},
608 | {:id => 3, :name => "Mexico", :language => "Spanish"}
609 | ]
610 | end
611 |
612 | it "returns an two dimensional Array of 3 attributes values" do
613 | expect(Country.pluck(:id, :name, :language)).to match_array([[1, "US", "English"], [2, "Canada", "English"], [3, "Mexico", "Spanish"]])
614 | end
615 |
616 | it "returns an two dimensional Array of 2 attributes values" do
617 | expect(Country.pluck(:id, :name)).to match_array([[1, "US"], [2, "Canada"], [3, "Mexico"]])
618 | end
619 |
620 | it "returns an Array of attribute values" do
621 | expect(Country.pluck(:id)).to match_array([1, 2, 3])
622 | end
623 |
624 | context 'with the same field name and method name' do
625 | before do
626 | class CountryWithContinent < ActiveHash::Base
627 | self.data = [
628 | {:id => 1, :name => "US", continent: 1},
629 | {:id => 2, :name => "Canada", continent: 1},
630 | {:id => 3, :name => "Mexico", continent: 1},
631 | {:id => 4, :name => "Brazil", continent: 2}
632 | ]
633 |
634 | # behave like ActiveRecord Enum
635 | CONTINENTS = { north_america: 1, south_america: 2, europe: 3, asia: 4, africa: 5, oceania: 6 }
636 |
637 | def continent
638 | CONTINENTS.key(self[:continent]).to_sym
639 | end
640 | end
641 | end
642 |
643 | it "returns the value of the method when the field name is the same as the method name" do
644 | expect(CountryWithContinent.pluck(:id, :name, :continent)).to match_array([[1, "US", :north_america], [2, "Canada", :north_america], [3, "Mexico", :north_america], [4, "Brazil", :south_america]])
645 | end
646 | end
647 | end
648 |
649 | describe '.ids' do
650 | before do
651 | Country.data = [
652 | {:id => 1, :name => "US"},
653 | {:id => 2, :name => "Canada"}
654 | ]
655 | end
656 |
657 | it "returns an Array of id attributes" do
658 | expect(Country.ids).to match_array([1,2])
659 | end
660 | end
661 |
662 | describe ".pick" do
663 | before do
664 | Country.data = [
665 | {:id => 1, :name => "US"},
666 | {:id => 2, :name => "Canada"}
667 | ]
668 | end
669 |
670 | it "returns a dimensional Array of attributes values" do
671 | expect(Country.pick(:id, :name)).to match_array([1,"US"])
672 | end
673 |
674 | it "returns an attribute value" do
675 | expect(Country.pick(:id)).to eq 1
676 | end
677 | end
678 |
679 | describe ".first" do
680 | before do
681 | Country.data = [
682 | {:id => 1, :name => "US"},
683 | {:id => 2, :name => "Canada"}
684 | ]
685 | end
686 |
687 | it "returns the first object" do
688 | expect(Country.first).to eq(Country.new(:id => 1))
689 | end
690 | end
691 |
692 | describe ".last" do
693 | before do
694 | Country.data = [
695 | {:id => 1, :name => "US"},
696 | {:id => 2, :name => "Canada"}
697 | ]
698 | end
699 |
700 | it "returns the last object" do
701 | expect(Country.last).to eq(Country.new(:id => 2))
702 | end
703 | end
704 |
705 | describe ".find" do
706 | before do
707 | Country.data = [
708 | {:id => 1, :name => "US"},
709 | {:id => 2, :name => "Canada"}
710 | ]
711 | end
712 |
713 | context "with an id" do
714 | it "finds the record with the specified id" do
715 | expect(Country.find(2).id).to eq(2)
716 | end
717 |
718 | it "finds the record with the specified id as a string" do
719 | expect(Country.find("2").id).to eq(2)
720 | end
721 |
722 | it "raises ActiveHash::RecordNotFound when id not found" do
723 | expect {
724 | Country.find(0)
725 | }.to raise_error(an_instance_of(ActiveHash::RecordNotFound)
726 | .and having_attributes(
727 | message: "Couldn't find Country with ID=0",
728 | primary_key: 'id',
729 | id: 0
730 | )
731 | )
732 | end
733 | end
734 |
735 | context "with :all" do
736 | it "returns all records" do
737 | expect(Country.find(:all)).to eq([Country.new(:id => 1), Country.new(:id => 2)])
738 | end
739 | end
740 |
741 | context "with :first" do
742 | it "returns the first record" do
743 | expect(Country.find(:first)).to eq(Country.new(:id => 1))
744 | end
745 |
746 | it "returns the first record that matches the search criteria" do
747 | expect(Country.find(:first, :conditions => {:id => 2})).to eq(Country.new(:id => 2))
748 | end
749 |
750 | it "returns nil if none matches the search criteria" do
751 | expect(Country.find(:first, :conditions => {:id => 3})).to eq(nil)
752 | end
753 | end
754 |
755 | context "with 2 arguments" do
756 | it "returns the record with the given id and ignores the conditions" do
757 | expect(Country.find(1, :conditions => "foo=bar")).to eq(Country.new(:id => 1))
758 | expect(Country.find(:all, :conditions => "foo=bar").length).to eq(2)
759 | end
760 | end
761 |
762 | context "with an array of ids" do
763 | before do
764 | Country.data = [
765 | {:id => 1},
766 | {:id => 2},
767 | {:id => 3}
768 | ]
769 | end
770 |
771 | it "returns all matching ids" do
772 | expect(Country.find([1, 3])).to eq([Country.new(:id => 1), Country.new(:id => 3)])
773 | end
774 |
775 | it "raises ActiveHash::RecordNotFound when id not found" do
776 | expect do
777 | Country.find([0, 3])
778 | end.to raise_error(ActiveHash::RecordNotFound, /Couldn't find Country with ID=0/)
779 | end
780 | end
781 |
782 | context "with nil" do
783 | context 'and no block' do
784 | it "raises ActiveHash::RecordNotFound when id is nil" do
785 | expect do
786 | Country.find(nil)
787 | end.to raise_error(ActiveHash::RecordNotFound, /Couldn't find Country without an ID/)
788 | end
789 | end
790 |
791 | context 'and a block' do
792 | it 'finds the record by evaluating the block' do
793 | country = Country.find { |c| c.id == 1 }
794 |
795 | expect(country).to be_a(Country)
796 | expect(country.name).to eq('US')
797 | end
798 | end
799 | end
800 | end
801 |
802 | describe ".find_by_id" do
803 | before do
804 | Country.data = [
805 | {:id => 1, :name => "US"},
806 | {:id => 2, :name => "Canada"}
807 | ]
808 | end
809 |
810 | context "with an id" do
811 | it "finds the record with the specified id" do
812 | expect(Country.find_by_id(2).id).to eq(2)
813 | end
814 |
815 | it "finds the record with the specified id as a string" do
816 | expect(Country.find_by_id("2").id).to eq(2)
817 | end
818 |
819 | it "finds the record with a chained filter" do
820 | expect(Country.where(name: "Canada").find_by_id("2").id).to eq(2)
821 | end
822 |
823 | it "filters ecord with a chained filter" do
824 | expect(Country.where(name: "Canada").find_by_id("1")).to be_nil
825 | end
826 | end
827 |
828 | context "with string ids" do
829 | before do
830 | Country.data = [
831 | {:id => "abc", :name => "US"},
832 | {:id => "def", :name => "Canada"}
833 | ]
834 | end
835 |
836 | it "finds the record with the specified id" do
837 | expect(Country.find_by_id("abc").id).to eq("abc")
838 | end
839 | end
840 |
841 | context "with nil" do
842 | it "returns nil" do
843 | expect(Country.find_by_id(nil)).to be_nil
844 | end
845 | end
846 |
847 | context "with an id not present" do
848 | it "returns nil" do
849 | expect(Country.find_by_id(4567)).to be_nil
850 | end
851 | end
852 | end
853 |
854 | describe "custom finders" do
855 | before do
856 | Country.fields :name, :monarch, :language
857 |
858 | # Start ids above 4 lest we get nil and think it's an AH::Base model with id=4.
859 | Country.data = [
860 | {:id => 11, :name => nil, :monarch => nil, :language => "Latin"},
861 | {:id => 12, :name => "US", :monarch => nil, :language => "English"},
862 | {:id => 13, :name => "Canada", :monarch => "The Crown of England", :language => "English"},
863 | {:id => 14, :name => "UK", :monarch => "The Crown of England", :language => "English"}
864 | ]
865 | end
866 |
867 | describe "find_by_" do
868 | describe "with a match" do
869 | context "for a non-nil argument" do
870 | it "returns the first matching record" do
871 | expect(Country.find_by_name("US").id).to eq(12)
872 | end
873 | end
874 |
875 | context "for a nil argument" do
876 | it "returns the first matching record" do
877 | expect(Country.find_by_name(nil).id).to eq(11)
878 | end
879 | end
880 | end
881 |
882 | describe "without a match" do
883 | before do
884 | Country.data = []
885 | end
886 |
887 | context "for a non-nil argument" do
888 | it "returns nil" do
889 | expect(Country.find_by_name("Mexico")).to be_nil
890 | end
891 | end
892 |
893 | context "for a nil argument" do
894 | it "returns nil" do
895 | expect(Country.find_by_name(nil)).to be_nil
896 | end
897 | end
898 | end
899 | end
900 |
901 | describe "find_by_!" do
902 | describe "with a match" do
903 | context "for a non-nil argument" do
904 | it "returns the first matching record" do
905 | expect(Country.find_by_name!("US").id).to eq(12)
906 | end
907 | end
908 |
909 | context "for a nil argument" do
910 | it "returns the first matching record" do
911 | expect(Country.find_by_name!(nil).id).to eq(11)
912 | end
913 | end
914 | end
915 |
916 | describe "without a match" do
917 | before do
918 | Country.data = []
919 | end
920 |
921 | context "for a non-nil argument" do
922 | it "raises ActiveHash::RecordNotFound" do
923 | expect { Country.find_by_name!("Mexico") }.to raise_error(ActiveHash::RecordNotFound, /Couldn't find Country with name = Mexico/)
924 | end
925 | end
926 |
927 | context "for a nil argument" do
928 | it "raises ActiveHash::RecordNotFound" do
929 | expect { Country.find_by_name!(nil) }.to raise_error(ActiveHash::RecordNotFound, /Couldn't find Country with name = /)
930 | end
931 | end
932 | end
933 | end
934 |
935 | describe "find_all_by_" do
936 | describe "with matches" do
937 | it "returns all matching records" do
938 | countries = Country.find_all_by_monarch("The Crown of England")
939 | expect(countries.length).to eq(2)
940 | expect(countries.first.name).to eq("Canada")
941 | expect(countries.last.name).to eq("UK")
942 | end
943 | end
944 |
945 | describe "without matches" do
946 | it "returns an empty array" do
947 | expect(Country.find_all_by_name("Mexico")).to be_empty
948 | end
949 | end
950 | end
951 |
952 | describe "find_by__and_" do
953 | describe "with a match" do
954 | it "returns the first matching record" do
955 | expect(Country.find_by_name_and_monarch("Canada", "The Crown of England").id).to eq(13)
956 | expect(Country.find_by_monarch_and_name("The Crown of England", "Canada").id).to eq(13)
957 | end
958 | end
959 |
960 | describe "with a match based on to_s" do
961 | it "returns the first matching record" do
962 | expect(Country.find_by_name_and_id("Canada", "13").id).to eq(13)
963 | end
964 | end
965 |
966 | describe "without a match" do
967 | it "returns nil" do
968 | expect(Country.find_by_name_and_monarch("US", "The Crown of England")).to be_nil
969 | end
970 | end
971 |
972 | describe "for fields the class doesn't have" do
973 | it "raises a NoMethodError" do
974 | expect {
975 | Country.find_by_name_and_shoe_size("US", 10)
976 | }.to raise_error(NoMethodError, /undefined method [`']find_by_name_and_shoe_size' (?:for|on) (class )?Country/)
977 | end
978 | end
979 | end
980 |
981 | describe "find_by__and_!" do
982 | describe "with a match" do
983 | it "returns the first matching record" do
984 | expect(Country.find_by_name_and_monarch!("Canada", "The Crown of England").id).to eq(13)
985 | expect(Country.find_by_monarch_and_name!("The Crown of England", "Canada").id).to eq(13)
986 | end
987 | end
988 |
989 | describe "with a match based on to_s" do
990 | it "returns the first matching record" do
991 | expect(Country.find_by_name_and_id!("Canada", "13").id).to eq(13)
992 | end
993 | end
994 |
995 | describe "without a match" do
996 | it "raises ActiveHash::RecordNotFound" do
997 | expect { Country.find_by_name_and_monarch!("US", "The Crown of England") }.to raise_error(ActiveHash::RecordNotFound, /Couldn't find Country with name = US, monarch = The Crown of England/)
998 | end
999 | end
1000 |
1001 | describe "for fields the class doesn't have" do
1002 | it "raises a NoMethodError" do
1003 | expect {
1004 | Country.find_by_name_and_shoe_size!("US", 10)
1005 | }.to raise_error(NoMethodError, /undefined method [`']find_by_name_and_shoe_size!' (?:for|on) (class )?Country/)
1006 | end
1007 | end
1008 | end
1009 |
1010 | describe "find_all_by__and_" do
1011 | describe "with matches" do
1012 | it "returns all matching records" do
1013 | countries = Country.find_all_by_monarch_and_language("The Crown of England", "English")
1014 | expect(countries.length).to eq(2)
1015 | expect(countries.first.name).to eq("Canada")
1016 | expect(countries.last.name).to eq("UK")
1017 | end
1018 | end
1019 |
1020 | describe "without matches" do
1021 | it "returns an empty array" do
1022 | expect(Country.find_all_by_monarch_and_language("Shaka Zulu", "Zulu")).to be_empty
1023 | end
1024 | end
1025 | end
1026 | end
1027 |
1028 | describe ".order" do
1029 | before do
1030 | Country.field :name
1031 | Country.field :language
1032 | Country.field :code
1033 | Country.data = [
1034 | { id: 1, name: "US", language: "English", code: 1 },
1035 | { id: 2, name: "Canada", language: "English", code: 1 },
1036 | { id: 3, name: "Mexico", language: "Spanish", code: 52 }
1037 | ]
1038 | end
1039 |
1040 | it "raises ArgumentError if no args are provieded" do
1041 | expect { Country.order() }.to raise_error(ArgumentError, 'The method .order() must contain arguments.')
1042 | end
1043 |
1044 | it "returns all records when passed nil" do
1045 | expect(Country.order(nil)).to eq Country.all
1046 | end
1047 |
1048 | it "returns all records when an empty hash" do
1049 | expect(Country.order({})).to eq Country.all
1050 | end
1051 |
1052 | it "returns all records ordered by name attribute in ASC order when ':name' is provieded" do
1053 | countries = Country.order(:name)
1054 | expect(countries.first).to eq Country.find_by(name: "Canada")
1055 | expect(countries.second).to eq Country.find_by(name: "Mexico")
1056 | expect(countries.third).to eq Country.find_by(name: "US")
1057 | end
1058 |
1059 | it "returns all records ordered by name attribute in DESC order when 'name: :desc' is provieded" do
1060 | countries = Country.order(name: :desc)
1061 | expect(countries.first).to eq Country.find_by(name: "US")
1062 | expect(countries.second).to eq Country.find_by(name: "Mexico")
1063 | expect(countries.third).to eq Country.find_by(name: "Canada")
1064 | end
1065 |
1066 | it "returns all records ordered by code attribute, followed by id attribute in DESC order when ':code, id: :desc' is provieded" do
1067 | countries = Country.order(:code, id: :desc)
1068 | expect(countries.first).to eq Country.find_by(name: "Canada")
1069 | expect(countries.second).to eq Country.find_by(name: "US")
1070 | expect(countries.third).to eq Country.find_by(name: "Mexico")
1071 | end
1072 |
1073 | it "returns all records ordered by name attribute in ASC order when 'name' is provieded" do
1074 | countries = Country.order("name")
1075 | expect(countries.first).to eq Country.find_by(name: "Canada")
1076 | expect(countries.second).to eq Country.find_by(name: "Mexico")
1077 | expect(countries.third).to eq Country.find_by(name: "US")
1078 | end
1079 |
1080 | it "returns all records ordered by name attribute in DESC order when 'name: :desc' is provieded" do
1081 | countries = Country.order("name DESC")
1082 | expect(countries.first).to eq Country.find_by(name: "US")
1083 | expect(countries.second).to eq Country.find_by(name: "Mexico")
1084 | expect(countries.third).to eq Country.find_by(name: "Canada")
1085 | end
1086 |
1087 | it "returns all records ordered by code attributes, followed by id attribute in DESC order when ':code, id: :desc' is provieded" do
1088 | countries = Country.order("code, id DESC")
1089 | expect(countries.first).to eq Country.find_by(name: "Canada")
1090 | expect(countries.second).to eq Country.find_by(name: "US")
1091 | expect(countries.third).to eq Country.find_by(name: "Mexico")
1092 | end
1093 |
1094 | it "populates the data correctly in the order provided" do
1095 | countries = Country.where(language: 'English').order(id: :desc)
1096 |
1097 | expect(countries.count).to eq 2
1098 | expect(countries.first).to eq Country.find_by(name: "Canada")
1099 | expect(countries.second).to eq Country.find_by(name: "US")
1100 | end
1101 |
1102 | it "can be chained" do
1103 | countries = Country.order(language: :asc)
1104 | expect(countries.first).to eq Country.find_by(name: "US")
1105 |
1106 | countries = countries.order(name: :asc)
1107 | expect(countries.first).to eq Country.find_by(name: "Canada")
1108 | end
1109 |
1110 | it "doesn't change the order of original records" do
1111 | countries = Country.order(id: :desc)
1112 |
1113 | expect(countries.first).to eq Country.find_by(name: "Mexico")
1114 | expect(countries.second).to eq Country.find_by(name: "Canada")
1115 | expect(countries.third).to eq Country.find_by(name: "US")
1116 |
1117 | expect(countries.find(1)).to eq Country.find_by(name: "US")
1118 |
1119 | expect(Country.all.first).to eq Country.find_by(name: "US")
1120 | expect(Country.all.second).to eq Country.find_by(name: "Canada")
1121 | expect(Country.all.third).to eq Country.find_by(name: "Mexico")
1122 | end
1123 | end
1124 |
1125 | describe ".reorder" do
1126 | it "re-orders records" do
1127 | countries = Country.order(language: :asc)
1128 | expect(countries.first).to eq Country.find_by(name: "US")
1129 |
1130 | countries = countries.reorder(id: :desc)
1131 | expect(countries.first).to eq Country.find_by(name: "Mexico")
1132 | end
1133 | end
1134 |
1135 | describe ".exists?" do
1136 | before do
1137 | Country.field :name
1138 | Country.field :language
1139 | Country.field :code
1140 | Country.data = [
1141 | { id: 1, name: "US", language: "English", code: 1 },
1142 | { id: 2, name: "Canada", language: "English", code: 1 },
1143 | { id: 3, name: "Mexico", language: "Spanish", code: 52 }
1144 | ]
1145 | end
1146 |
1147 | context "when data are exists and no arguments is passed" do
1148 | it "return true" do
1149 | expect(Country.exists?).to be_truthy
1150 | end
1151 | end
1152 |
1153 | context "when no data are exists and no arguments is passed" do
1154 | before do
1155 | Country.field :name
1156 | Country.field :language
1157 | Country.field :code
1158 | Country.data = []
1159 | end
1160 |
1161 | it "return false" do
1162 | expect(Country.exists?).to be_falsy
1163 | end
1164 | end
1165 |
1166 | context "when false is passed" do
1167 | it "return false" do
1168 | expect(Country.exists?(false)).to be_falsy
1169 | end
1170 | end
1171 |
1172 | context "when nil is passed" do
1173 | it "return nil" do
1174 | expect(Country.exists?(nil)).to be_falsy
1175 | end
1176 | end
1177 |
1178 | describe "with matches" do
1179 | context 'for a record argument' do
1180 | it "return true" do
1181 | expect(Country.exists?(Country.new({ id: 1, name: "US", language: "English", code: 1 }))).to be_truthy
1182 | end
1183 | end
1184 |
1185 | context "for an integer argument" do
1186 | it "return true" do
1187 | expect(Country.exists?(1)).to be_truthy
1188 | end
1189 | end
1190 |
1191 | context "for a string argument" do
1192 | it "return true" do
1193 | expect(Country.exists?("1")).to be_truthy
1194 | end
1195 | end
1196 |
1197 | context "for a hash argument" do
1198 | it "return true" do
1199 | expect(Country.exists?(name: "US", language: "English")).to be_truthy
1200 | end
1201 | end
1202 | end
1203 |
1204 | describe "without matches" do
1205 | context 'for a record argument' do
1206 | it "return false" do
1207 | expect(Country.exists?(Country.new({ id: 4, name: "Franch", language: "French", code: 16 }))).to be_falsy
1208 | end
1209 | end
1210 |
1211 | context "for an integer argument" do
1212 | it "return false" do
1213 | expect(Country.exists?(4)).to be_falsy
1214 | end
1215 | end
1216 |
1217 | context "for a string argument" do
1218 | it "return false" do
1219 | expect(Country.exists?("4")).to be_falsy
1220 | end
1221 | end
1222 |
1223 | context "for a hash argument" do
1224 | it "return false" do
1225 | expect(Country.exists?(name: "US", language: "Spanish")).to be_falsy
1226 | end
1227 | end
1228 | end
1229 | end
1230 |
1231 | describe "#method_missing" do
1232 | it "doesn't blow up if you call a missing dynamic finder when fields haven't been set" do
1233 | expect do
1234 | Country.find_by_name("Foo")
1235 | end.to raise_error(NoMethodError, /undefined method [`']find_by_name' (?:for|on) (class )?Country/)
1236 | end
1237 | end
1238 |
1239 | describe "#attributes" do
1240 | it "returns the hash passed in the initializer" do
1241 | Country.field :foo
1242 | country = Country.new(:foo => :bar)
1243 | expect(country.attributes).to eq({:foo => :bar})
1244 | end
1245 |
1246 | it "symbolizes keys" do
1247 | Country.field :foo
1248 | country = Country.new("foo" => :bar)
1249 | expect(country.attributes).to eq({:foo => :bar})
1250 | end
1251 |
1252 | it "works with #[]" do
1253 | Country.field :foo
1254 | country = Country.new(:foo => :bar)
1255 | expect(country[:foo]).to eq(:bar)
1256 | end
1257 |
1258 | it "works with _read_attribute" do
1259 | Country.field :foo
1260 | country = Country.new(:foo => :bar)
1261 | expect(country._read_attribute(:foo)).to eq(:bar)
1262 | end
1263 |
1264 | it "works when string key passed to _read_attribute" do
1265 | Country.field :foo
1266 | country = Country.new(:foo => :bar)
1267 | expect(country._read_attribute('foo')).to eq(:bar)
1268 | end
1269 |
1270 | it "works with read_attribute" do
1271 | Country.field :foo
1272 | country = Country.new(:foo => :bar)
1273 | expect(country.read_attribute(:foo)).to eq(:bar)
1274 | end
1275 |
1276 | it "works when string key passed to read_attribute" do
1277 | Country.field :foo
1278 | country = Country.new(:foo => :bar)
1279 | expect(country.read_attribute('foo')).to eq(:bar)
1280 | end
1281 |
1282 | it "works with #[]=" do
1283 | Country.field :foo
1284 | country = Country.new
1285 | country[:foo] = :bar
1286 | expect(country.foo).to eq(:bar)
1287 | end
1288 | end
1289 |
1290 | describe "reader methods" do
1291 | context "for regular fields" do
1292 | before do
1293 | Country.fields :name, :iso_name
1294 | end
1295 |
1296 | it "returns the given attribute when present" do
1297 | country = Country.new(:name => "Spain")
1298 | expect(country.name).to eq("Spain")
1299 | end
1300 |
1301 | it "returns nil when not present" do
1302 | country = Country.new
1303 | expect(country.name).to be_nil
1304 | end
1305 | end
1306 |
1307 | context "for fields with default values" do
1308 | before do
1309 | Country.field :name, :default => "foobar"
1310 | end
1311 |
1312 | it "returns the given attribute when present" do
1313 | country = Country.new(:name => "Spain")
1314 | expect(country.name).to eq("Spain")
1315 | end
1316 |
1317 | it "returns the default value when not present" do
1318 | country = Country.new
1319 | expect(country.name).to eq("foobar")
1320 | end
1321 |
1322 | context "#attributes" do
1323 | it "returns the default value when not present" do
1324 | country = Country.new
1325 | expect(country.attributes[:name]).to eq("foobar")
1326 | end
1327 |
1328 | context "when the default value is false" do
1329 | before do
1330 | Country.field :active, :default => false
1331 | end
1332 |
1333 | it "returns the default value when not present" do
1334 | country = Country.new
1335 | expect(country.attributes[:active]).to eq(false)
1336 | end
1337 | end
1338 | end
1339 | end
1340 | end
1341 |
1342 | describe "interrogator methods" do
1343 | before do
1344 | Country.fields :name, :iso_name
1345 | end
1346 |
1347 | it "returns true if the given attribute is non-blank" do
1348 | country = Country.new(:name => "Spain")
1349 | expect(country).to be_name
1350 | end
1351 |
1352 | it "returns false if the given attribute is blank" do
1353 | country = Country.new(:name => " ")
1354 | expect(country.name?).to eq(false)
1355 | end
1356 |
1357 | it "returns false if the given attribute was not passed" do
1358 | country = Country.new
1359 | expect(country).not_to be_name
1360 | end
1361 | end
1362 |
1363 | describe "#id" do
1364 | context "when not passed an id" do
1365 | it "returns nil" do
1366 | country = Country.new
1367 | expect(country.id).to be_nil
1368 | end
1369 | end
1370 | end
1371 |
1372 | describe "#quoted_id" do
1373 | it "should return id" do
1374 | expect(Country.new(:id => 2).quoted_id).to eq(2)
1375 | end
1376 | end
1377 |
1378 | describe "#to_param" do
1379 | it "should return id as a string" do
1380 | expect(Country.create(:id => 2).to_param).to eq("2")
1381 | end
1382 | end
1383 |
1384 | describe "#persisted" do
1385 | it "should return true if the object has been saved" do
1386 | expect(Country.create(:id => 2)).to be_persisted
1387 | end
1388 |
1389 | it "should return false if the object has not been saved" do
1390 | expect(Country.new(:id => 2)).not_to be_persisted
1391 | end
1392 | end
1393 |
1394 | describe "#persisted" do
1395 | it "should return true if the object has been saved" do
1396 | expect(Country.create(:id => 2)).to be_persisted
1397 | end
1398 |
1399 | it "should return false if the object has not been saved" do
1400 | expect(Country.new(:id => 2)).not_to be_persisted
1401 | end
1402 | end
1403 |
1404 | describe "#eql?" do
1405 | before do
1406 | class Region < ActiveHash::Base
1407 | end
1408 | end
1409 |
1410 | it "should return true with the same class and id" do
1411 | expect(Country.new(:id => 23).eql?(Country.new(:id => 23))).to be_truthy
1412 | end
1413 |
1414 | it "should return false with the same class and different ids" do
1415 | expect(Country.new(:id => 24).eql?(Country.new(:id => 23))).to be_falsey
1416 | end
1417 |
1418 | it "should return false with the different classes and the same id" do
1419 | expect(Country.new(:id => 23).eql?(Region.new(:id => 23))).to be_falsey
1420 | end
1421 |
1422 | it "returns false when id is nil" do
1423 | expect(Country.new.eql?(Country.new)).to be_falsey
1424 | end
1425 | end
1426 |
1427 | describe "#==" do
1428 | before do
1429 | class Region < ActiveHash::Base
1430 | end
1431 | end
1432 |
1433 | it "should return true with the same class and id" do
1434 | expect(Country.new(:id => 23)).to eq(Country.new(:id => 23))
1435 | end
1436 |
1437 | it "should return false with the same class and different ids" do
1438 | expect(Country.new(:id => 24)).not_to eq(Country.new(:id => 23))
1439 | end
1440 |
1441 | it "should return false with the different classes and the same id" do
1442 | expect(Country.new(:id => 23)).not_to eq(Region.new(:id => 23))
1443 | end
1444 |
1445 | it "returns false when id is nil" do
1446 | expect(Country.new).not_to eq(Country.new)
1447 | end
1448 | end
1449 |
1450 | describe "#hash" do
1451 | it "returns id for hash" do
1452 | expect(Country.new(:id => 45).hash).to eq(45.hash)
1453 | expect(Country.new.hash).to eq(nil.hash)
1454 | end
1455 |
1456 | it "is hashable" do
1457 | expect({Country.new(:id => 4) => "bar"}).to eq({Country.new(:id => 4) => "bar"})
1458 | expect({Country.new(:id => 3) => "bar"}).not_to eq({Country.new(:id => 4) => "bar"})
1459 | end
1460 | end
1461 |
1462 | describe "#readonly?" do
1463 | it "returns true" do
1464 | expect(Country.new).to be_readonly
1465 | end
1466 | end
1467 |
1468 | describe "auto-discovery of fields" do
1469 | it "dynamically creates fields for all keys in the hash" do
1470 | Country.data = [
1471 | {:field1 => "foo"},
1472 | {:field2 => "bar"},
1473 | {:field3 => "biz"}
1474 | ]
1475 |
1476 | [:field1, :field2, :field3].each do |field|
1477 | expect(Country).to respond_to("find_by_#{field}")
1478 | expect(Country).to respond_to("find_all_by_#{field}")
1479 | expect(Country.new).to respond_to(field)
1480 | expect(Country.new).to respond_to("#{field}?")
1481 | end
1482 | end
1483 |
1484 | it "doesn't override methods already defined" do
1485 | Country.class_eval do
1486 | class << self
1487 | def find_by_name(name)
1488 | "find_by_name defined manually"
1489 | end
1490 |
1491 | def find_all_by_name(name)
1492 | "find_all_by_name defined manually"
1493 | end
1494 | end
1495 |
1496 | def name
1497 | "name defined manually"
1498 | end
1499 |
1500 | def name?
1501 | "name? defined manually"
1502 | end
1503 | end
1504 |
1505 | expect(Country.find_by_name("foo")).to eq("find_by_name defined manually")
1506 | expect(Country.find_all_by_name("foo")).to eq("find_all_by_name defined manually")
1507 | expect(Country.new.name).to eq("name defined manually")
1508 | expect(Country.new.name?).to eq("name? defined manually")
1509 |
1510 | Country.data = [
1511 | {:name => "foo"}
1512 | ]
1513 |
1514 | Country.all
1515 | expect(Country.find_by_name("foo")).to eq("find_by_name defined manually")
1516 | expect(Country.find_all_by_name("foo")).to eq("find_all_by_name defined manually")
1517 | expect(Country.new.name).to eq("name defined manually")
1518 | expect(Country.new.name?).to eq("name? defined manually")
1519 | end
1520 | end
1521 |
1522 | describe "using with belongs_to in ActiveRecord", :unless => SKIP_ACTIVE_RECORD do
1523 | before do
1524 | Country.data = [
1525 | {:id => 1, :name => "foo"}
1526 | ]
1527 |
1528 | class Book < ActiveRecord::Base
1529 | establish_connection :adapter => "sqlite3", :database => ":memory:"
1530 | connection.create_table(:books, :force => true) do |t|
1531 | t.text :subject_type
1532 | t.integer :subject_id
1533 | t.integer :country_id
1534 | end
1535 |
1536 | extend ActiveHash::Associations::ActiveRecordExtensions
1537 |
1538 | belongs_to :subject, :polymorphic => true
1539 | belongs_to :country
1540 | end
1541 | end
1542 |
1543 | after do
1544 | Object.send :remove_const, :Book
1545 | end
1546 |
1547 | it "should be possible to use it as a parent" do
1548 | book = Book.new
1549 | book.country = Country.first
1550 | expect(book.country).to eq(Country.first)
1551 | end
1552 |
1553 | it "should be possible to use it as a polymorphic parent" do
1554 | book = Book.new
1555 | book.subject = Country.first
1556 | expect(book.subject).to eq(Country.first)
1557 | end
1558 |
1559 | end
1560 |
1561 | describe "#cache_key" do
1562 | it 'should use the class\'s cache_key and id' do
1563 | Country.data = [
1564 | {:id => 1, :name => "foo"}
1565 | ]
1566 |
1567 | expect(Country.first.cache_key).to eq('countries/1')
1568 | end
1569 |
1570 | it 'should use the record\'s updated_at if present' do
1571 | timestamp = Time.now
1572 |
1573 | Country.data = [
1574 | {:id => 1, :name => "foo", :updated_at => timestamp}
1575 | ]
1576 |
1577 | if ActiveSupport::VERSION::MAJOR < 7
1578 | expect(Country.first.cache_key).to eq("countries/1-#{timestamp.to_s(:number)}")
1579 | else
1580 | expect(Country.first.cache_key).to eq("countries/1-#{timestamp.to_fs(:number)}")
1581 | end
1582 | end
1583 |
1584 | it 'should use "new" instead of the id for a new record' do
1585 | expect(Country.new(:id => 1).cache_key).to eq('countries/new')
1586 | end
1587 | end
1588 |
1589 | describe "#save" do
1590 |
1591 | before do
1592 | Country.field :name
1593 | end
1594 |
1595 | it "adds the new object to the data collection" do
1596 | expect(Country.all).to be_empty
1597 | country = Country.new :id => 1, :name => "foo"
1598 | expect(country.save).to be_truthy
1599 | expect(Country.all).to eq([country])
1600 | end
1601 |
1602 | it "adds the new object to the data collection" do
1603 | expect(Country.all).to be_empty
1604 | country = Country.new :id => 1, :name => "foo"
1605 | expect(country.save!).to be_truthy
1606 | expect(Country.all).to eq([country])
1607 | end
1608 |
1609 | it "marks the class as dirty" do
1610 | expect(Country.dirty).to be_falsey
1611 | Country.new(:id => 1, :name => "foo").save
1612 | expect(Country.dirty).to be_truthy
1613 | end
1614 |
1615 | it "it is a no-op if the object has already been added to the collection" do
1616 | expect(Country.all).to be_empty
1617 | country = Country.new :id => 1, :name => "foo"
1618 | country.save
1619 | country.name = "bar"
1620 | country.save
1621 | country.save!
1622 | expect(Country.all).to eq([country])
1623 | end
1624 |
1625 | end
1626 |
1627 | describe ".create" do
1628 |
1629 | before do
1630 | Country.field :name
1631 | end
1632 |
1633 | it "works with no args" do
1634 | expect(Country.all).to be_empty
1635 | country = Country.create
1636 | expect(country.id).to eq(1)
1637 | end
1638 |
1639 | it "adds the new object to the data collection" do
1640 | expect(Country.all).to be_empty
1641 | country = Country.create :id => 1, :name => "foo"
1642 | expect(country.id).to eq(1)
1643 | expect(country.name).to eq("foo")
1644 | expect(Country.all).to eq([country])
1645 | end
1646 |
1647 | it "adds an auto-incrementing id if the id is nil" do
1648 | country1 = Country.new :name => "foo"
1649 | country1.save
1650 | expect(country1.id).to eq(1)
1651 |
1652 | country2 = Country.new :name => "bar"
1653 | country2.save
1654 | expect(country2.id).to eq(2)
1655 | end
1656 |
1657 | it "does not add auto-incrementing id if the id is present" do
1658 | country1 = Country.new :id => 456, :name => "foo"
1659 | country1.save
1660 | expect(country1.id).to eq(456)
1661 | end
1662 |
1663 | it "does not blow up with strings" do
1664 | country1 = Country.new :id => "foo", :name => "foo"
1665 | country1.save
1666 | expect(country1.id).to eq("foo")
1667 |
1668 | country2 = Country.new :name => "foo"
1669 | country2.save
1670 | expect(country2.id).to be_nil
1671 | end
1672 |
1673 | it "adds the new object to the data collection" do
1674 | expect(Country.all).to be_empty
1675 | country = Country.create! :id => 1, :name => "foo"
1676 | expect(country.id).to eq(1)
1677 | expect(country.name).to eq("foo")
1678 | expect(Country.all).to eq([country])
1679 | end
1680 |
1681 | it "marks the class as dirty" do
1682 | expect(Country.dirty).to be_falsey
1683 | Country.create! :id => 1, :name => "foo"
1684 | expect(Country.dirty).to be_truthy
1685 | end
1686 |
1687 | end
1688 |
1689 | describe "#valid?" do
1690 |
1691 | it "should return true" do
1692 | expect(Country.new).to be_valid
1693 | end
1694 |
1695 | end
1696 |
1697 | describe "#new_record?" do
1698 | before do
1699 | Country.field :name
1700 | Country.data = [
1701 | :id => 1, :name => "foo"
1702 | ]
1703 | end
1704 |
1705 | it "returns false when the object is already part of the collection" do
1706 | expect(Country.new(:id => 1)).not_to be_new_record
1707 | end
1708 |
1709 | it "returns true when the object is not part of the collection" do
1710 | expect(Country.new(:id => 2)).to be_new_record
1711 | end
1712 |
1713 | end
1714 |
1715 | describe ".transaction" do
1716 |
1717 | it "execute the block given to it" do
1718 | foo = Object.new
1719 | expect(foo).to receive(:bar)
1720 | Country.transaction do
1721 | foo.bar
1722 | end
1723 | end
1724 |
1725 | it "swallows ActiveRecord::Rollback errors", :unless => SKIP_ACTIVE_RECORD do
1726 | expect do
1727 | Country.transaction do
1728 | raise ActiveRecord::Rollback
1729 | end
1730 | end.not_to raise_error
1731 | end
1732 |
1733 | it "passes other errors through" do
1734 | expect do
1735 | Country.transaction do
1736 | raise "hell"
1737 | end
1738 | end.to raise_error("hell")
1739 | end
1740 |
1741 | end
1742 |
1743 | describe ".delete_all" do
1744 |
1745 | it "clears out all record" do
1746 | country1 = Country.create
1747 | country2 = Country.create
1748 | expect(Country.all).to eq([country1, country2])
1749 | Country.delete_all
1750 | expect(Country.all).to be_empty
1751 | end
1752 |
1753 | it "marks the class as dirty" do
1754 | expect(Country.dirty).to be_falsey
1755 | Country.delete_all
1756 | expect(Country.dirty).to be_truthy
1757 | end
1758 |
1759 | end
1760 |
1761 | describe '.scope' do
1762 | context 'for query without argument' do
1763 | before do
1764 | Country.field :name
1765 | Country.field :language
1766 | Country.data = [
1767 | {:id => 1, :name => "US", continent: 'North America', :language => 'English'},
1768 | {:id => 2, :name => "Canada" , continent: 'North America', :language => 'English'},
1769 | {:id => 3, :name => "Mexico" , continent: 'North America', :language => 'Spanish'},
1770 | {:id => 4, :name => "Brazil" , continent: 'South America', :language => 'Portuguese'}
1771 | ]
1772 | Country.scope :english_language, -> { where(language: 'English') }
1773 | Country.scope :portuguese_language, -> { where(language: 'Portuguese') }
1774 | Country.scope :south_america, -> { where(continent: 'South America') }
1775 | end
1776 |
1777 | it 'should define a scope method' do
1778 | expect(Country.respond_to?(:english_language)).to be_truthy
1779 | end
1780 |
1781 | it 'should return the query used to define the scope' do
1782 | expect(Country.english_language).to eq Country.where(language: 'English')
1783 | end
1784 |
1785 | it 'should behave like the query used to define the scope' do
1786 | expect(Country.english_language.count).to eq 2
1787 | expect(Country.english_language.first.id).to eq 1
1788 | expect(Country.english_language.second.id).to eq 2
1789 | end
1790 |
1791 | it 'should be chainable' do
1792 | expect(Country.south_america.portuguese_language).to(
1793 | eq(Country.where(continent: 'South America').where(language: 'Portuguese'))
1794 | )
1795 | end
1796 | end
1797 |
1798 | context 'for query with argument' do
1799 | before do
1800 | Country.field :name
1801 | Country.field :language
1802 | Country.data = [
1803 | {:id => 1, :name => "US", continent: 'North America', :language => 'English'},
1804 | {:id => 2, :name => "Canada", continent: 'North America', :language => 'English'},
1805 | {:id => 3, :name => "Mexico", continent: 'North America', :language => 'Spanish'},
1806 | {:id => 4, :name => "Brazil" , continent: 'South America', :language => 'Portuguese'}
1807 | ]
1808 | Country.scope :with_continent, ->(continent) { where(continent: continent) }
1809 | Country.scope :with_language, ->(language) { where(language: language) }
1810 | end
1811 |
1812 | it 'should define a scope method' do
1813 | expect(Country.respond_to?(:with_language)).to be_truthy
1814 | end
1815 |
1816 | it 'should return the query used to define the scope' do
1817 | expect(Country.with_language('English')).to eq Country.where(language: 'English')
1818 | end
1819 |
1820 | it 'should behave like the query used to define the scope' do
1821 | expect(Country.with_language('English').count).to eq 2
1822 | expect(Country.with_language('English').first.id).to eq 1
1823 | expect(Country.with_language('English').second.id).to eq 2
1824 | end
1825 |
1826 | it 'should be chainable' do
1827 | expect(Country.with_continent('South America').with_language('Portuguese')).to(
1828 | eq(Country.where(continent: 'South America').where(language: 'Portuguese'))
1829 | )
1830 | end
1831 | end
1832 |
1833 | context 'when scope body is not a lambda' do
1834 | before do
1835 | Country.field :name
1836 | Country.field :language
1837 | Country.data = [
1838 | {:id => 1, :name => "US", :language => 'English'},
1839 | {:id => 2, :name => "Canada", :language => 'English'},
1840 | {:id => 3, :name => "Mexico", :language => 'Spanish'}
1841 | ]
1842 | end
1843 |
1844 | it 'should raise an error' do
1845 | expect { Country.scope :invalid_scope, :not_a_callable }.to raise_error(ArgumentError, 'body needs to be callable')
1846 | end
1847 | end
1848 | end
1849 |
1850 | describe 'ActiveModel::Translation' do
1851 | around(:example) do |example|
1852 | if Object.const_defined?(:ActiveModel)
1853 | example.run
1854 | else
1855 | skip
1856 | end
1857 | end
1858 |
1859 | context 'if the locale is set to :ja' do
1860 | around(:example) do |example|
1861 | current_locale = I18n.locale
1862 | I18n.locale = :ja
1863 |
1864 | example.run
1865 |
1866 | I18n.locale = current_locale
1867 | end
1868 |
1869 | subject { Country.model_name.human }
1870 |
1871 | it { is_expected.to eq('国') }
1872 | end
1873 | end
1874 | end
1875 |
--------------------------------------------------------------------------------
/spec/active_hash/relation_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | RSpec.describe ActiveHash::Relation do
4 | let(:model_class) do
5 | Class.new(ActiveHash::Base) do
6 | self.data = [
7 | {:id => 1, :name => "US"},
8 | {:id => 2, :name => "Canada"}
9 | ]
10 | end
11 | end
12 |
13 | subject { model_class.all }
14 |
15 | describe '#sample' do
16 | it 'delegate `sample` to Array' do
17 | expect(subject).to respond_to(:sample)
18 | end
19 |
20 | it 'return a random element or n random elements' do
21 | records = subject
22 |
23 | expect(records.sample).to be_present
24 | expect(records.sample(2).count).to eq(2)
25 | end
26 | end
27 |
28 | describe '#to_ary' do
29 | it 'returns an array' do
30 | expect(subject.to_ary).to be_an(Array)
31 | end
32 |
33 | it 'contains the same items as the relation' do
34 | array = subject.to_ary
35 |
36 | expect(array.length).to eq(subject.count)
37 | expect(array.first.id).to eq(1)
38 | expect(array.second.id).to eq(2)
39 | end
40 | end
41 |
42 | describe '#count' do
43 | it 'supports a block arg' do
44 | expect(subject.count { |s| s.name == "US" }).to eq(1)
45 | end
46 |
47 | it 'returns the correct number of items of the relation' do
48 | expect(subject.count).to eq(2)
49 | end
50 | end
51 |
52 | describe '#size' do
53 | it 'returns an Integer' do
54 | expect(subject.size).to be_an(Integer)
55 | end
56 |
57 | it 'returns the correct number of items of the relation' do
58 | array = subject.to_ary
59 |
60 | expect(array.size).to eq(2)
61 | end
62 | end
63 |
64 | describe "colliding methods https://github.com/active-hash/active_hash/issues/280" do
65 | it "should handle attributes named after existing methods" do
66 | klass = Class.new(ActiveHash::Base) do
67 | self.data = [
68 | {
69 | id: 1,
70 | name: "Aaa",
71 | display: true,
72 | },
73 | {
74 | id: 2,
75 | name: "Bbb",
76 | display: false,
77 | },
78 | ]
79 | end
80 |
81 | expect(klass.where(display: true).length).to eq(1)
82 | end
83 | end
84 |
85 | describe "#pretty_print" do
86 | it "prints the records" do
87 | out = StringIO.new
88 | PP.pp(subject, out)
89 |
90 | expect(out.string.scan(/\bid\b/).length).to eq(2)
91 | expect(out.string).to match(/\bCanada\b/)
92 | expect(out.string).to match(/\bUS\b/)
93 | expect(out.string).to_not match(/ActiveHash::Relation/)
94 | end
95 | end
96 | end
97 |
--------------------------------------------------------------------------------
/spec/active_json/base_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ActiveJSON::Base do
4 |
5 | before do
6 | ActiveJSON::Base.set_root_path File.expand_path(File.dirname(__FILE__) + "/../fixtures")
7 |
8 | class ArrayRow < ActiveJSON::Base; end
9 | class City < ActiveJSON::Base; end
10 | class State < ActiveJSON::Base; end
11 | class Empty < ActiveJSON::Base ; end # Empty JSON
12 | end
13 |
14 | after do
15 | Object.send :remove_const, :ArrayRow
16 | Object.send :remove_const, :City
17 | Object.send :remove_const, :State
18 | Object.send :remove_const, :Empty
19 | end
20 |
21 | describe ".load_path" do
22 | it 'can execute embedded ruby' do
23 | expect(State.first.name).to match(/^New York/)
24 | end
25 |
26 | it 'can load empty yaml' do
27 | expect(Empty.first).to be_nil
28 | end
29 | end
30 |
31 | describe ".all" do
32 | context "before the file is loaded" do
33 | it "reads from the file" do
34 | expect(State.all).not_to be_empty
35 | expect(State.count).to be > 0
36 | end
37 | end
38 | end
39 |
40 | describe ".where" do
41 | context "before the file is loaded" do
42 | it "reads from the file and filters by where statement" do
43 | expect(State.where(:name => 'Oregon')).not_to be_empty
44 | expect(State.count).to be > 0
45 | end
46 | end
47 | end
48 |
49 | describe ".delete_all" do
50 | context "when called before .all" do
51 | it "causes all to not load data" do
52 | State.delete_all
53 | expect(State.all).to be_empty
54 | end
55 | end
56 |
57 | context "when called after .all" do
58 | it "clears out the data" do
59 | expect(State.all).not_to be_empty
60 | State.delete_all
61 | expect(State.all).to be_empty
62 | end
63 | end
64 | end
65 |
66 | describe ".raw_data" do
67 | it "returns the raw array data loaded from yaml array-formatted files" do
68 | expect(ArrayRow.raw_data).to be_kind_of(Array)
69 | end
70 | end
71 |
72 | describe ".load_file" do
73 | describe "with array data" do
74 | it "returns an array of hashes" do
75 | expect(ArrayRow.load_file).to be_kind_of(Array)
76 | expect(ArrayRow.load_file).to include({"name" => "Row 1", "id" => 1})
77 | end
78 | end
79 |
80 | describe "with hash data" do
81 | it "returns an array of hashes" do
82 | expect(City.load_file).to be_kind_of(Array)
83 | expect(City.load_file).to include({"state" => "New York", "name" => "Albany", "id" => 1})
84 | City.reload
85 | expect(City.all).to include(City.new(:id => 1))
86 | end
87 | end
88 | end
89 |
90 | describe 'ID finders without reliance on a call to all, even with fields specified' do
91 |
92 | before do
93 | class City < ActiveJSON::Base
94 | fields :id, :state, :name
95 | end
96 | end
97 |
98 | it 'returns a single city based on #find' do
99 | expect(City.find(1).name).to eq('Albany')
100 | end
101 |
102 | it 'returns a single city based on #find with a block' do
103 | expect(City.find { |c| c.id == 1 }.name).to eq('Albany')
104 | end
105 |
106 | it 'returns a single city based on find_by_id' do
107 | expect(City.find_by_id(1).name).to eq('Albany')
108 | end
109 |
110 | end
111 |
112 | describe 'meta programmed finders and properties for fields that exist in the JSON file' do
113 |
114 | it 'should have a finder method for each property' do
115 | expect(City.find_by_state('Oregon')).not_to be_nil
116 | end
117 |
118 | it 'should have a find all method for each property' do
119 | expect(City.find_all_by_state('Oregon')).not_to be_nil
120 | end
121 |
122 | end
123 |
124 | describe "multiple files" do
125 | context "given array files" do
126 | before do
127 | class Country < ActiveJSON::Base
128 | use_multiple_files
129 | set_filenames 'countries', 'commonwealths'
130 | end
131 | end
132 | after { Object.send :remove_const, :Country }
133 |
134 | it "loads data from both files" do
135 | # countries.yml
136 | expect(Country.find_by_name("Canada")).not_to be_nil
137 |
138 | # commonwealths.yml
139 | expect(Country.find_by_name("Puerto Rico")).not_to be_nil
140 | end
141 | end
142 |
143 | context "given hash files" do
144 | before do
145 | class MultiState < ActiveJSON::Base
146 | use_multiple_files
147 | set_filenames 'states', 'provinces'
148 | end
149 | end
150 |
151 | after do
152 | Object.send(:remove_const, :MultiState)
153 | end
154 |
155 | it "loads data from both files" do
156 | # states.yml
157 | expect(MultiState.find_by_name("Oregon")).not_to be_nil
158 |
159 | # provinces.yml
160 | expect(MultiState.find_by_name("British Colombia")).not_to be_nil
161 | end
162 | end
163 |
164 | context "given a hash and an array file" do
165 | before do
166 | class Municipality < ActiveJSON::Base
167 | use_multiple_files
168 | set_filenames 'states', 'countries'
169 | end
170 | end
171 | after { Object.send(:remove_const, :Municipality) }
172 |
173 | it "raises an exception" do
174 | expect do
175 | Municipality.find_by_name("Oregon")
176 | end.to raise_error(ActiveHash::FileTypeMismatchError)
177 | end
178 | end
179 | end
180 | end
181 |
--------------------------------------------------------------------------------
/spec/active_yaml/aliases_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ActiveYaml::Aliases do
4 |
5 | before do
6 | ActiveYaml::Base.set_root_path File.expand_path(File.dirname(__FILE__) + "/../fixtures")
7 |
8 | class ArrayProduct < ActiveYaml::Base
9 | include ActiveYaml::Aliases
10 | end
11 |
12 | class KeyProduct < ActiveYaml::Base
13 | include ActiveYaml::Aliases
14 | end
15 | end
16 |
17 | after do
18 | Object.send :remove_const, :ArrayProduct
19 | Object.send :remove_const, :KeyProduct
20 | end
21 |
22 | context 'using yaml arrays' do
23 | let(:model) { ArrayProduct }
24 |
25 | describe '.all' do
26 | specify { expect(model.all.length).to eq 4 }
27 | end
28 |
29 | describe 'aliased attributes' do
30 | subject { model.where(:name => 'Coke').first.attributes }
31 |
32 | it('sets strings correctly') { expect(subject[:flavor]).to eq('sweet') }
33 | it('sets floats correctly') { expect(subject[:price]).to eq(1.0) }
34 | end
35 |
36 | describe 'keys starting with "/"' do
37 | it 'excludes them from records' do
38 | models_including_aliases = model.all.select { |p| p.attributes.keys.include? :'/aliases' }
39 | expect(models_including_aliases).to be_empty
40 | end
41 |
42 | it 'excludes them from fields' do
43 | model.all
44 | expect(model.field_names).to match_array [:name, :flavor, :price]
45 | end
46 |
47 | it 'excludes them from column_names' do
48 | skip "Not supported in Ruby 3.0.0" if RUBY_VERSION < "3.0.0"
49 | model.all
50 | expect(model.column_names).to match_array ["name", "flavor", "price"]
51 | end
52 | end
53 | end
54 |
55 | context 'with YAML hashes' do
56 | let(:model) { KeyProduct }
57 |
58 | describe '.all' do
59 | specify { expect(model.all.length).to eq 4 }
60 | end
61 |
62 | describe 'aliased attributes' do
63 | subject { model.where(:name => 'Coke').first.attributes }
64 |
65 | it('sets strings correctly') { expect(subject[:flavor]).to eq('sweet') }
66 | it('sets floats correctly') { expect(subject[:price]).to eq(1.0) }
67 | end
68 |
69 | describe 'keys starting with "/"' do
70 | it 'excludes them from records' do
71 | models_including_aliases = model.all.select { |p| p.attributes.keys.include? :'/aliases' }
72 | expect(models_including_aliases).to be_empty
73 | end
74 |
75 | it 'excludes them from fields' do
76 | model.all
77 | expect(model.field_names).to match_array [:name, :flavor, :price, :slogan, :key]
78 | end
79 |
80 | it 'excludes them from column_names' do
81 | skip "Not supported in Ruby 3.0.0" if RUBY_VERSION < "3.0.0"
82 | model.all
83 | expect(model.column_names).to match_array ["name", "flavor", "price", "slogan", "key"]
84 | end
85 | end
86 | end
87 |
88 | describe 'Loading multiple files' do
89 | let(:model) { MultipleFiles }
90 | let(:coke) { model.where(:name => 'Coke').first }
91 | let(:schweppes) { model.where(:name => 'Schweppes').first }
92 |
93 | before do
94 | class MultipleFiles < ActiveYaml::Base
95 | include ActiveYaml::Aliases
96 | use_multiple_files
97 | set_filenames 'array_products', 'array_products_2'
98 | end
99 | end
100 |
101 | after do
102 | Object.send :remove_const, :MultipleFiles
103 | end
104 |
105 | it 'returns correct data from both files' do
106 | expect(coke.flavor).to eq 'sweet'
107 | expect(schweppes.flavor).to eq 'bitter'
108 | end
109 | end
110 | end
111 |
--------------------------------------------------------------------------------
/spec/active_yaml/base_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ActiveYaml::Base do
4 |
5 | before do
6 | ActiveYaml::Base.set_root_path File.expand_path(File.dirname(__FILE__) + "/../fixtures")
7 |
8 | ENV['USER_PASSWORD'] = 'secret'
9 |
10 | class ArrayRow < ActiveYaml::Base ; end
11 | class City < ActiveYaml::Base ; end
12 | class State < ActiveYaml::Base ; end
13 | class ArrayProduct < ActiveYaml::Base ; end # Contain YAML aliases
14 | class KeyProduct < ActiveYaml::Base ; end # Contain YAML aliases
15 | class User < ActiveYaml::Base ; end # Contain ERB (embedded ruby)
16 | class Empty < ActiveYaml::Base ; end # Empty YAML
17 | end
18 |
19 | after do
20 | Object.send :remove_const, :ArrayRow
21 | Object.send :remove_const, :City
22 | Object.send :remove_const, :State
23 | Object.send :remove_const, :User
24 | Object.send :remove_const, :Empty
25 | end
26 |
27 | describe ".load_path" do
28 | context 'default' do
29 | it 'can execute embedded ruby' do
30 | expect(User.first.email).to match /^user[0-9]*@email.com$/
31 | expect(User.first.password).to eq 'secret'
32 | end
33 | end
34 |
35 | context 'erb disabled' do
36 | before { ActiveYaml::Base.process_erb = false }
37 | after { ActiveYaml::Base.process_erb = true }
38 |
39 | it 'can execute embedded ruby' do
40 | expect(User.first.email).to eq '<%= "user#{rand(100)}@email.com" %>'
41 | expect(User.first.password).to eq "<%= ENV['USER_PASSWORD'] %>"
42 | end
43 | end
44 |
45 | it 'can load empty yaml' do
46 | expect(Empty.first).to be_nil
47 | end
48 |
49 | it 'is thread-safe' do
50 | (1..5).map do
51 | Thread.new { expect(City.count).to eq(3) }
52 | end.each(&:join)
53 | end
54 | end
55 |
56 | describe ".all" do
57 | context "before the file is loaded" do
58 | it "reads from the file" do
59 | expect(State.all).not_to be_empty
60 | expect(State.count).to be > 0
61 | end
62 | end
63 | end
64 |
65 | describe ".where" do
66 | context "before the file is loaded" do
67 | it "reads from the file and filters by where statement" do
68 | expect(State.where(:name => 'Oregon')).not_to be_empty
69 | expect(State.count).to be > 0
70 | end
71 | end
72 | end
73 |
74 | describe ".delete_all" do
75 | context "when called before .all" do
76 | it "causes all to not load data" do
77 | State.delete_all
78 | expect(State.all).to be_empty
79 | end
80 | end
81 |
82 | context "when called after .all" do
83 | it "clears out the data" do
84 | expect(State.all).not_to be_empty
85 | State.delete_all
86 | expect(State.all).to be_empty
87 | end
88 | end
89 | end
90 |
91 | describe ".raw_data" do
92 |
93 | it "returns the raw hash data loaded from yaml hash-formatted files" do
94 | expect(City.raw_data).to be_kind_of(Hash)
95 | expect(City.raw_data.keys).to include("albany", "portland")
96 | end
97 |
98 | it "returns the raw array data loaded from yaml array-formatted files" do
99 | expect(ArrayRow.raw_data).to be_kind_of(Array)
100 | end
101 |
102 | end
103 |
104 | describe ".load_file" do
105 |
106 | describe "with array data" do
107 | it "returns an array of hashes" do
108 | expect(ArrayRow.load_file).to be_kind_of(Array)
109 | expect(ArrayRow.load_file).to include({"name" => "Row 1", "id" => 1})
110 | end
111 | end
112 |
113 | describe "with hash data" do
114 | it "returns an array of hashes" do
115 | expect(City.load_file).to be_kind_of(Array)
116 | expect(City.load_file).to include({"state" => :new_york, "name" => "Albany", "id" => 1, "key" => "albany"})
117 | City.reload
118 | expect(City.all).to include(City.new(:id => 1))
119 | end
120 |
121 | it "automatically adds the key attribute" do
122 | expect(City.load_file.first.keys).to include("key")
123 | end
124 |
125 | it "doesn't overwrite the key attribute when specifically listed in the yml file" do
126 | expect(City.load_file.last["key"]).to eql("livable")
127 | end
128 |
129 | it "uses the root key of the hash if no key attribute is specified" do
130 | expect(City.load_file.first["key"]).to eql("albany")
131 | end
132 | end
133 |
134 | end
135 |
136 | describe 'ID finders without reliance on a call to all, even with fields specified' do
137 |
138 | before do
139 | class City < ActiveYaml::Base
140 | fields :id, :state, :name
141 | end
142 | end
143 |
144 | it 'returns a single city based on #find' do
145 | expect(City.find(1).name).to eq('Albany')
146 | end
147 |
148 | it 'returns a single city based on #find with a block' do
149 | expect(City.find { |c| c.id == 1 }.name).to eq('Albany')
150 | end
151 |
152 | it 'returns a single city based on find_by_id' do
153 | expect(City.find_by_id(1).name).to eq('Albany')
154 | end
155 |
156 | end
157 |
158 | describe 'meta programmed finders and properties for fields that exist in the YAML' do
159 |
160 | it 'should have a finder method for each property' do
161 | expect(City.find_by_state('Oregon')).not_to be_nil
162 | end
163 |
164 | it 'should have a find all method for each property' do
165 | expect(City.find_all_by_state('Oregon')).not_to be_nil
166 | end
167 |
168 | end
169 |
170 | describe "multiple files" do
171 | context "given array files" do
172 | before do
173 | class Country < ActiveYaml::Base
174 | use_multiple_files
175 | set_filenames 'countries', 'commonwealths'
176 | end
177 | end
178 | after { Object.send :remove_const, :Country }
179 |
180 | it "loads data from both files" do
181 | # countries.yml
182 | expect(Country.find_by_name("Canada")).not_to be_nil
183 |
184 | # commonwealths.yml
185 | expect(Country.find_by_name("Puerto Rico")).not_to be_nil
186 | end
187 | end
188 |
189 | context "given hash files" do
190 | before do
191 | class MultiState < ActiveYaml::Base
192 | use_multiple_files
193 | set_filenames 'states', 'provinces'
194 | end
195 | end
196 |
197 | after do
198 | Object.send(:remove_const, :MultiState)
199 | end
200 |
201 | it "loads data from both files" do
202 | # states.yml
203 | expect(MultiState.find_by_name("Oregon")).not_to be_nil
204 |
205 | # provinces.yml
206 | expect(MultiState.find_by_name("British Colombia")).not_to be_nil
207 | end
208 | end
209 |
210 | context "given a hash and an array file" do
211 | before do
212 | class Municipality < ActiveYaml::Base
213 | use_multiple_files
214 | set_filenames 'states', 'countries'
215 | end
216 | end
217 | after { Object.send :remove_const, :Municipality }
218 |
219 | it "raises an exception" do
220 | expect do
221 | Municipality.find_by_name("Oregon")
222 | end.to raise_error(ActiveHash::FileTypeMismatchError)
223 | end
224 | end
225 | end
226 | end
227 |
--------------------------------------------------------------------------------
/spec/associations/active_record_extensions_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | unless SKIP_ACTIVE_RECORD
4 | describe ActiveHash::Base, "active record extensions" do
5 |
6 | def define_ephemeral_class(name, superclass, &block)
7 | klass = Class.new(superclass)
8 | Object.const_set(name, klass)
9 | klass.class_eval(&block) if block_given?
10 | @ephemeral_classes << name
11 | end
12 |
13 | def define_book_classes
14 | define_ephemeral_class(:Author, ActiveHash::Base) do
15 | include ActiveHash::Associations
16 | end
17 |
18 | define_ephemeral_class(:Book, ActiveRecord::Base) do
19 | establish_connection :adapter => "sqlite3", :database => ":memory:"
20 | connection.create_table(:books, :force => true) do |t|
21 | t.integer :author_id
22 | t.integer :author_code
23 | t.boolean :published
24 | end
25 |
26 | if Object.const_defined?(:ActiveModel)
27 | scope(:published, proc { where(:published => true) })
28 | else
29 | named_scope :published, {:conditions => {:published => true}}
30 | end
31 | end
32 | end
33 |
34 | def define_person_classes
35 | define_ephemeral_class(:Country, ActiveHash::Base) do
36 | self.data = [
37 | {:id => 1, :name => "Japan"}
38 | ]
39 | end
40 |
41 | define_ephemeral_class(:Person, ActiveRecord::Base) do
42 | establish_connection :adapter => "sqlite3", :database => ":memory:"
43 | connection.create_table(:people, :force => true) do |t|
44 | end
45 |
46 | extend ActiveHash::Associations::ActiveRecordExtensions
47 | end
48 |
49 | define_ephemeral_class(:Post, ActiveRecord::Base) do
50 | establish_connection :adapter => "sqlite3", :database => ":memory:"
51 | connection.create_table(:posts, :force => true) do |t|
52 | t.integer :person_id
53 | t.datetime :created_at
54 | end
55 |
56 | belongs_to :person
57 | end
58 | end
59 |
60 | def define_school_classes
61 | define_ephemeral_class(:Country, ActiveRecord::Base) do
62 | establish_connection :adapter => "sqlite3", :database => ":memory:"
63 | connection.create_table(:countries, :force => true) do |t|
64 | t.string :name
65 | end
66 | extend ActiveHash::Associations::ActiveRecordExtensions
67 | end
68 |
69 | define_ephemeral_class(:School, ActiveRecord::Base) do
70 | establish_connection :adapter => "sqlite3", :database => ":memory:"
71 | connection.create_table(:schools, :force => true) do |t|
72 | t.integer :country_id
73 | t.string :locateable_type
74 | t.integer :locateable_id
75 | t.integer :city_id
76 | end
77 |
78 | extend ActiveHash::Associations::ActiveRecordExtensions
79 | end
80 |
81 | define_ephemeral_class(:City, ActiveHash::Base) do
82 | include ActiveHash::Associations
83 | end
84 |
85 | define_ephemeral_class(:SchoolStatus, ActiveHash::Base)
86 | end
87 |
88 | def define_doctor_classes
89 | define_ephemeral_class(:Physician, ActiveHash::Base) do
90 | include ActiveHash::Associations
91 |
92 | has_many :appointments
93 | has_many :patients, through: :appointments
94 |
95 | self.data = [
96 | {:id => 1, :name => "ikeda"},
97 | {:id => 2, :name => "sato"}
98 | ]
99 | end
100 |
101 | define_ephemeral_class(:Appointment, ActiveRecord::Base) do
102 | establish_connection :adapter => "sqlite3", :database => ":memory:"
103 | connection.create_table :appointments, force: true do |t|
104 | t.references :physician
105 | t.references :patient
106 | end
107 |
108 | extend ActiveHash::Associations::ActiveRecordExtensions
109 |
110 | belongs_to :physician
111 | belongs_to :patient
112 | end
113 |
114 | define_ephemeral_class(:Patient, ActiveRecord::Base) do
115 | establish_connection :adapter => "sqlite3", :database => ":memory:"
116 | connection.create_table :patients, force: true do |t|
117 | end
118 |
119 | extend ActiveHash::Associations::ActiveRecordExtensions
120 |
121 | has_many :appointments
122 | has_many :physicians, through: :appointments
123 | end
124 |
125 | end
126 |
127 | before do
128 | @ephemeral_classes = []
129 | end
130 |
131 | after do
132 | @ephemeral_classes.each do |klass_name|
133 | Object.send :remove_const, klass_name
134 | end
135 | end
136 |
137 | describe "#has_many" do
138 | context "with ActiveRecord children" do
139 | before { define_book_classes }
140 |
141 | context "with default options" do
142 | before do
143 | @book_1 = Book.create! :author_id => 1, :published => true
144 | @book_2 = Book.create! :author_id => 1, :published => false
145 | @book_3 = Book.create! :author_id => 2, :published => true
146 | Author.has_many :books
147 | end
148 |
149 | it "find the correct records" do
150 | author = Author.create :id => 1
151 | expect(author.books).to eq([@book_1, @book_2])
152 | end
153 |
154 | it "should find the correct record ids" do
155 | author = Author.create :id => 1
156 | expect(author.book_ids).to eq([@book_1.id, @book_2.id])
157 | end
158 |
159 | it "return a scope so that we can apply further scopes" do
160 | author = Author.create :id => 1
161 | expect(author.books.published).to eq([@book_1])
162 | end
163 | end
164 |
165 | context "with a primary_key option" do
166 | before do
167 | @book_1 = Book.create! :author_id => 1, :published => true
168 | @book_2 = Book.create! :author_id => 2, :published => false
169 | @book_3 = Book.create! :author_id => 2, :published => true
170 | Author.field :book_identifier
171 | Author.has_many :books, :primary_key => :book_identifier
172 | end
173 |
174 | it "should find the correct records" do
175 | author = Author.create :id => 1, :book_identifier => 2
176 | expect(author.books).to eq([@book_2, @book_3])
177 | end
178 |
179 | it "should find the correct record ids" do
180 | author = Author.create :id => 1, :book_identifier => 2
181 | expect(author.book_ids).to eq([@book_2.id, @book_3.id])
182 | end
183 |
184 | it "return a scope so that we can apply further scopes" do
185 | author = Author.create :id => 1, :book_identifier => 2
186 | expect(author.books.published).to eq([@book_3])
187 | end
188 | end
189 |
190 | context "with a foreign_key option" do
191 | before do
192 | @book_1 = Book.create! :author_code => 1, :published => true
193 | @book_2 = Book.create! :author_code => 1, :published => false
194 | @book_3 = Book.create! :author_code => 2, :published => true
195 | Author.has_many :books, :foreign_key => :author_code
196 | end
197 |
198 | it "should find the correct records" do
199 | author = Author.create :id => 1
200 | expect(author.books).to eq([@book_1, @book_2])
201 | end
202 |
203 | it "should find the correct record ids" do
204 | author = Author.create :id => 1
205 | expect(author.book_ids).to eq([@book_1.id, @book_2.id])
206 | end
207 |
208 | it "return a scope so that we can apply further scopes" do
209 | author = Author.create :id => 1
210 | expect(author.books.published).to eq([@book_1])
211 | end
212 | end
213 |
214 | it "only uses 1 query" do
215 | Author.has_many :books
216 | author = Author.create :id => 1
217 | expect(Book).to receive(:where).with(author_id: 1).once.and_call_original
218 | author.books.to_a
219 | end
220 | end
221 |
222 | describe ":through" do
223 | before { define_doctor_classes }
224 |
225 | it "finds ActiveHash records through the join model" do
226 | patient = Patient.create!
227 |
228 | physician1 = Physician.first
229 | Appointment.create!(physician: physician1, patient: patient)
230 | Appointment.create!(physician: physician1, patient: patient)
231 |
232 | physician2 = Physician.last
233 | Appointment.create!(physician: physician2, patient: patient)
234 |
235 | expect(patient.physicians).to contain_exactly(physician1, physician2)
236 | end
237 | end
238 |
239 | describe "with a lambda" do
240 | before do
241 | define_person_classes
242 | now = Time.now
243 | @post_1 = Post.create! :person_id => 1, :created_at => now
244 | @post_2 = Post.create! :person_id => 1, :created_at => 1.day.ago
245 | Post.create! :person_id => 2, :created_at => now
246 | Person.has_many :posts, lambda { order(created_at: :asc) }
247 | end
248 |
249 | it "should find the correct records" do
250 | person = Person.create :id => 1
251 | expect(person.posts).to eq([@post_2, @post_1])
252 | end
253 | end
254 | end
255 |
256 | describe ActiveHash::Associations::ActiveRecordExtensions do
257 | describe "#belongs_to" do
258 | before { define_school_classes }
259 |
260 | it "doesn't interfere with AR's procs in belongs_to methods" do
261 | School.belongs_to :country, lambda { where(name: 'Japan') }
262 | school = School.new
263 | country = Country.create!(id: 1, name: 'Japan')
264 | school.country = country
265 | expect(school.country).to eq(country)
266 | expect(school.country_id).to eq(country.id)
267 | expect(school.country).to eq(country)
268 | school.save!
269 | school.reload
270 | expect(school.country_id).to eq(country.id)
271 | expect(school.country).to eq(country)
272 |
273 | country.update!(name: 'JAPAN')
274 | school.reload
275 | expect(school.country_id).to eq(country.id)
276 | expect(school.country).to eq(nil)
277 | end
278 |
279 | it "doesn't interfere with AR's belongs_to arguments" do
280 | allow(ActiveRecord::Base).to receive(:belongs_to).with(:country, nil)
281 | allow(ActiveRecord::Base).to receive(:belongs_to).with(:country, nil, {})
282 |
283 | School.belongs_to :country
284 | end
285 |
286 | it "doesn't interfere w/ ActiveRecord's polymorphism" do
287 | School.belongs_to :locateable, :polymorphic => true
288 | school = School.new
289 | country = Country.create!
290 | school.locateable = country
291 | expect(school.locateable).to eq(country)
292 | school.save!
293 | expect(school.reload.locateable_id).to eq(country.id)
294 | end
295 |
296 | it "sets up an ActiveRecord association for non-ActiveHash objects" do
297 | School.belongs_to :country
298 | school = School.new
299 | country = Country.create!
300 | school.country = country
301 | expect(school.country).to eq(country)
302 | expect(school.country_id).to eq(country.id)
303 | school.save!
304 | school.reload
305 | expect(school.reload.country_id).to eq(country.id)
306 | end
307 |
308 | it "calls through to belongs_to_active_hash if it's an ActiveHash object" do
309 | School.belongs_to :city
310 | city = City.create
311 | school = School.create :city_id => city.id
312 | expect(school.city).to eq(city)
313 | end
314 |
315 | it "doesn't raise any exception when the belongs_to association class can't be autoloaded" do
316 | # Simulate autoloader
317 | allow_any_instance_of(String).to receive(:constantize).and_raise(LoadError, "Unable to autoload constant NonExistent")
318 | expect { School.belongs_to :city, class_name: 'NonExistent' }.not_to raise_error
319 | end
320 | end
321 |
322 | describe "#belongs_to_active_hash" do
323 | before { define_school_classes }
324 |
325 | context "setting by id" do
326 | it "finds the correct records" do
327 | School.belongs_to_active_hash :city
328 | city = City.create
329 | school = School.create :city_id => city.id
330 | expect(school.city).to eq(city)
331 | end
332 |
333 | it "returns nil when the record does not exist" do
334 | School.belongs_to_active_hash :city
335 | school = School.create! :city_id => nil
336 | expect(school.city).to be_nil
337 | end
338 | end
339 |
340 | context "setting by association" do
341 | it "finds the correct records" do
342 | School.belongs_to_active_hash :city
343 | city = City.create
344 | school = School.create :city => city
345 | expect(school.city).to eq(city)
346 | end
347 |
348 | it "is assignable by name attribute" do
349 | School.belongs_to_active_hash :city, :shortcuts => [:name]
350 | City.data = [{:id => 1, :name => 'gothan'}]
351 | city = City.find_by_name 'gothan'
352 | school = School.create :city_name => 'gothan'
353 | expect(school.city).to eq(city)
354 | expect(school.city_name).to eq('gothan')
355 | end
356 |
357 | it "have custom shortcut" do
358 | School.belongs_to_active_hash :city, :shortcuts => :friendly_name
359 | City.data = [{:id => 1, :friendly_name => 'Gothan City'}]
360 | city = City.find_by_friendly_name 'Gothan City'
361 | school = School.create :city_friendly_name => 'Gothan City'
362 | expect(school.city).to eq(city)
363 | expect(school.city_friendly_name).to eq('Gothan City')
364 | end
365 |
366 | it "returns nil when the record does not exist" do
367 | School.belongs_to_active_hash :city
368 | school = School.create! :city => nil
369 | expect(school.city).to be_nil
370 | end
371 | end
372 |
373 | it "finds active record metadata for this association" do
374 | School.belongs_to_active_hash :city
375 | association = School.reflect_on_association(:city)
376 | expect(association).not_to be_nil
377 | expect(association.klass.name).to eq(City.name)
378 | end
379 |
380 | it "handles classes ending with an 's'" do
381 | School.belongs_to_active_hash :school_status
382 | association = School.reflect_on_association(:school_status)
383 | expect(association).not_to be_nil
384 | expect(association.klass.name).to eq(SchoolStatus.name)
385 | end
386 |
387 | it "handles custom association names" do
388 | School.belongs_to_active_hash :status, :class_name => 'SchoolStatus'
389 | association = School.reflect_on_association(:status)
390 | expect(association).not_to be_nil
391 | expect(association.klass.name).to eq(SchoolStatus.name)
392 | end
393 | end
394 | end
395 |
396 | describe "#belongs_to" do
397 | context "with an ActiveRecord parent" do
398 | before { define_school_classes }
399 |
400 | it "find the correct records" do
401 | City.belongs_to :country
402 | country = Country.create
403 | city = City.create :country_id => country.id
404 | expect(city.country).to eq(country)
405 | end
406 |
407 | it "returns nil when the record does not exist" do
408 | City.belongs_to :country
409 | city = City.create :country_id => 123
410 | expect(city.country).to be_nil
411 | end
412 | end
413 | end
414 |
415 | describe "#has_one" do
416 | context "with ActiveRecord children" do
417 | before do
418 | define_book_classes
419 | Author.has_one :book
420 | end
421 |
422 | it "find the correct records" do
423 | book = Book.create! :author_id => 1, :published => true
424 | author = Author.create :id => 1
425 | expect(author.book).to eq(book)
426 | end
427 |
428 | it "returns nil when there is no record" do
429 | author = Author.create :id => 1
430 | expect(author.book).to be_nil
431 | end
432 | end
433 | end
434 | end
435 | end
436 |
--------------------------------------------------------------------------------
/spec/associations/associations_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ActiveHash::Base, "associations" do
4 |
5 | before do
6 | class City < ActiveHash::Base
7 | include ActiveHash::Associations
8 | end
9 |
10 | class Author < ActiveHash::Base
11 | include ActiveHash::Associations
12 | end
13 |
14 | class SchoolStatus < ActiveHash::Base
15 | end
16 | end
17 |
18 | after do
19 | Object.send :remove_const, :City
20 | Object.send :remove_const, :Author
21 | Object.send :remove_const, :SchoolStatus
22 | end
23 |
24 | describe "#has_many" do
25 |
26 | context "with ActiveHash children" do
27 | context "with default options" do
28 | before do
29 | Author.field :city_id
30 | @included_author_1 = Author.create :city_id => 1
31 | @included_author_2 = Author.create :city_id => 1
32 | @excluded_author = Author.create :city_id => 2
33 | end
34 |
35 | it "find the correct records" do
36 | City.has_many :authors
37 | city = City.create :id => 1
38 | expect(city.authors).to eq([@included_author_1, @included_author_2])
39 | end
40 |
41 | it "uses the correct class name when passed" do
42 | City.has_many :writers, :class_name => "Author"
43 | city = City.create :id => 1
44 | expect(city.writers).to eq([@included_author_1, @included_author_2])
45 | end
46 | end
47 |
48 | context "with a primary_key option" do
49 | before do
50 | Author.field :city_id
51 | City.field :author_identifier
52 | @author_1 = Author.create :city_id => 1
53 | @author_2 = Author.create :city_id => 10
54 | @author_3 = Author.create :city_id => 10
55 | City.has_many :authors, :primary_key => :author_identifier
56 | end
57 |
58 | it "finds the correct records" do
59 | city = City.create :id => 1, :author_identifier => 10
60 | expect(city.authors).to eq([@author_2, @author_3])
61 | end
62 | end
63 |
64 | context "with a foreign_key option" do
65 | before do
66 | Author.field :city_id
67 | Author.field :city_identifier
68 | @author_1 = Author.create :city_id => 1, :city_identifier => 10
69 | @author_2 = Author.create :city_id => 10, :city_identifier => 10
70 | @author_3 = Author.create :city_id => 10, :city_identifier => 5
71 | City.has_many :authors, :foreign_key => :city_identifier
72 | end
73 |
74 | it "finds the correct records" do
75 | city = City.create :id => 10
76 | expect(city.authors).to eq([@author_1, @author_2])
77 | end
78 | end
79 | end
80 |
81 | end
82 |
83 | describe "#belongs_to" do
84 |
85 | context "with an ActiveHash parent" do
86 | it "find the correct records" do
87 | Author.belongs_to :city
88 | city = City.create
89 | author = Author.create :city_id => city.id
90 | expect(author.city).to eq(city)
91 | end
92 |
93 | it "returns nil when the record does not exist" do
94 | Author.belongs_to :city
95 | author = Author.create :city_id => 123
96 | expect(author.city).to be_nil
97 | end
98 | end
99 |
100 | describe "#parent=" do
101 | before do
102 | Author.belongs_to :city
103 | @city = City.create :id => 1
104 | end
105 |
106 | it "sets the underlying id of the parent" do
107 | author = Author.new
108 | author.city = @city
109 | expect(author.city_id).to eq(@city.id)
110 | end
111 |
112 | it "works from hash assignment" do
113 | author = Author.new :city => @city
114 | expect(author.city_id).to eq(@city.id)
115 | expect(author.city).to eq(@city)
116 | end
117 |
118 | it "works with nil" do
119 | author = Author.new :city => @city
120 | expect(author.city_id).to eq(@city.id)
121 | expect(author.city).to eq(@city)
122 |
123 | author.city = nil
124 | expect(author.city_id).to be_nil
125 | expect(author.city).to be_nil
126 | end
127 | end
128 |
129 | describe "with a different foreign key" do
130 | before do
131 | Author.belongs_to :residence, :class_name => "City", :foreign_key => "city_id"
132 | @city = City.create :id => 1
133 | end
134 |
135 | it "works" do
136 | author = Author.new
137 | author.residence = @city
138 | expect(author.city_id).to eq(@city.id)
139 | end
140 | end
141 |
142 | describe "with a different primary key" do
143 | before do
144 | City.field :long_identifier
145 | Author.belongs_to :city, :primary_key => "long_identifier"
146 | @city = City.create :id => 1, :long_identifier => "123"
147 | end
148 |
149 | it "works" do
150 | author = Author.new
151 | author.city = @city
152 | expect(author.city_id).to eq(@city.long_identifier)
153 | end
154 | end
155 | end
156 |
157 | describe "#has_one" do
158 | context "with ActiveHash children" do
159 | context "with default options" do
160 | before do
161 | Author.field :city_id
162 | City.has_one :author
163 | end
164 |
165 | it "find the correct records" do
166 | author = Author.create :city_id => 1
167 | city = City.create :id => 1
168 | expect(city.author).to eq(author)
169 | end
170 |
171 | it "returns nil when there are no records" do
172 | Author.create :city_id => 10
173 | city = City.create :id => 1
174 | expect(city.author).to be_nil
175 | end
176 | end
177 |
178 | context "with a primary_key option" do
179 | before do
180 | Author.field :city_id
181 | City.field :author_identifier
182 | City.has_one :author, :primary_key => :author_identifier
183 | end
184 |
185 | it "find the correct records" do
186 | Author.create :city_id => 1
187 | author = Author.create :city_id => 10
188 | city = City.create :id => 1, :author_identifier => 10
189 | expect(city.author).to eq(author)
190 | end
191 |
192 | it "returns nil when there are no records" do
193 | Author.create :city_id => 1
194 | city = City.create :id => 1, :author_identifier => 10
195 | expect(city.author).to be_nil
196 | end
197 | end
198 |
199 | context "with a foreign_key option" do
200 | before do
201 | Author.field :city_id
202 | Author.field :city_identifier
203 | City.has_one :author, :foreign_key => :city_identifier
204 | end
205 |
206 | it "find the correct records" do
207 | Author.create :city_id => 1, :city_identifier => 1
208 | author = Author.create :city_id => 1, :city_identifier => 10
209 | Author.create :city_id => 10, :city_identifier => 5
210 | city = City.create :id => 10
211 | expect(city.author).to eq(author)
212 | end
213 |
214 | it "returns nil when there are no records" do
215 | Author.create :city_id => 1, :city_identifier => 1
216 | Author.create :city_id => 10, :city_identifier => 5
217 | city = City.create :id => 10
218 | expect(city.author).to be_nil
219 | end
220 | end
221 | end
222 | end
223 |
224 | describe "#marked_for_destruction?" do
225 | it "should return false" do
226 | expect(City.new.marked_for_destruction?).to eq(false)
227 | end
228 | end
229 |
230 | end
231 |
--------------------------------------------------------------------------------
/spec/enum/enum_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe ActiveHash::Base, "enum" do
4 |
5 | before do
6 | ActiveYaml::Base.set_root_path File.expand_path(File.dirname(__FILE__) + "/../fixtures")
7 |
8 | class Borough < ActiveYaml::Base
9 | include ActiveHash::Enum
10 | fields :name, :county, :population
11 | enum_accessor :name
12 | end
13 |
14 | class Neighborhood < ActiveHash::Base
15 | include ActiveHash::Enum
16 | fields :name, :county
17 | enum_accessor :name, :county
18 |
19 | self.data = [
20 | {:name => "Queen Ann", :county => "King"}
21 | ]
22 | end
23 | end
24 |
25 | after do
26 | Object.send(:remove_const, :Borough)
27 | Object.send(:remove_const, :Neighborhood)
28 | end
29 |
30 | describe "#enum_accessor" do
31 | it "can use a custom method" do
32 | expect(Borough::BROOKLYN).to eq(Borough.find_by_name("Brooklyn"))
33 | end
34 |
35 | it "sets the field used for accessing records by constants" do
36 | expect(Neighborhood::QUEEN_ANN_KING).to eq(Neighborhood.find_by_name("Queen Ann"))
37 | end
38 |
39 | it "ensures that values stored in the field specified are unique" do
40 | expect do
41 | Class.new(ActiveHash::Base) do
42 | include ActiveHash::Enum
43 | self.data = [
44 | {:name => 'Woodford Reserve'},
45 | {:name => 'Bulliet Bourbon'},
46 | {:name => 'Woodford Reserve'}
47 | ]
48 | enum_accessor :name
49 | end
50 | end.to raise_error(ActiveHash::Enum::DuplicateEnumAccessor)
51 | end
52 |
53 | it "can use enum accessor constant with same name as top-level constant" do
54 | expect do
55 | Class.new(ActiveHash::Base) do
56 | include ActiveHash::Enum
57 | self.data = [
58 | {:type => 'JSON'},
59 | {:type => 'YAML'},
60 | {:type => 'XML'}
61 | ]
62 | enum_accessor :type
63 | end
64 | end.not_to raise_error
65 | end
66 |
67 | it "removes non-word characters from values before setting constants" do
68 | Movie = Class.new(ActiveHash::Base) do
69 | include ActiveHash::Enum
70 | self.data = [
71 | {:name => 'Die Hard 2', :rating => '4.3'},
72 | {:name => 'The Informant!', :rating => '4.3'},
73 | {:name => 'In & Out', :rating => '4.3'}
74 | ]
75 | enum_accessor :name
76 | end
77 |
78 | expect(Movie::DIE_HARD_2.name).to eq('Die Hard 2')
79 | expect(Movie::THE_INFORMANT.name).to eq('The Informant!')
80 | expect(Movie::IN_OUT.name).to eq('In & Out')
81 | end
82 |
83 | describe "enum(columns)" do
84 | it "defines a predicate method for each value in the enum" do
85 | Article = Class.new(ActiveHash::Base) do
86 | include ActiveHash::Enum
87 |
88 | self.data = [
89 | { name: 'Article 1', status: 'draft'},
90 | { name: 'Article 2', status: 'published'},
91 | { name: 'Article 3', status: 'archived'}
92 | ]
93 |
94 | enum_accessor :name
95 |
96 | enum status: [:draft, :published, :archived]
97 | end
98 |
99 | expect(Article::ARTICLE_1.draft?).to be_truthy
100 | expect(Article::ARTICLE_1.published?).to be_falsey
101 | expect(Article::ARTICLE_1.archived?).to be_falsey
102 |
103 | expect(Article::ARTICLE_2.draft?).to be_falsey
104 | expect(Article::ARTICLE_2.published?).to be_truthy
105 | expect(Article::ARTICLE_2.archived?).to be_falsey
106 |
107 | expect(Article::ARTICLE_3.draft?).to be_falsey
108 | expect(Article::ARTICLE_3.published?).to be_falsey
109 | expect(Article::ARTICLE_3.archived?).to be_truthy
110 | end
111 |
112 | it "multi type data (ex: string, integer and symbol) enum" do
113 | NotifyType = Class.new(ActiveHash::Base) do
114 | include ActiveHash::Enum
115 |
116 | self.data = [
117 | { name: 'Like', action: 'LIKE'},
118 | { name: 'Comment', action: 1},
119 | { name: 'Follow', action: :FOLLOW},
120 | { name: 'Mention', action: 'MENTION'}
121 | ]
122 |
123 | enum_accessor :name
124 |
125 | enum action: { like: 'LIKE', comment: 1, follow: :FOLLOW, mention: 'MENTION' }
126 | end
127 |
128 | expect(NotifyType::LIKE.like?).to be_truthy
129 | expect(NotifyType::LIKE.comment?).to be_falsey
130 | expect(NotifyType::LIKE.follow?).to be_falsey
131 | expect(NotifyType::LIKE.mention?).to be_falsey
132 |
133 | expect(NotifyType::COMMENT.like?).to be_falsey
134 | expect(NotifyType::COMMENT.comment?).to be_truthy
135 | expect(NotifyType::COMMENT.follow?).to be_falsey
136 | expect(NotifyType::COMMENT.mention?).to be_falsey
137 |
138 | expect(NotifyType::FOLLOW.like?).to be_falsey
139 | expect(NotifyType::FOLLOW.comment?).to be_falsey
140 | expect(NotifyType::FOLLOW.follow?).to be_truthy
141 | expect(NotifyType::FOLLOW.mention?).to be_falsey
142 |
143 | expect(NotifyType::MENTION.like?).to be_falsey
144 | expect(NotifyType::MENTION.comment?).to be_falsey
145 | expect(NotifyType::MENTION.follow?).to be_falsey
146 | expect(NotifyType::MENTION.mention?).to be_truthy
147 | end
148 | end
149 | end
150 |
151 | context "ActiveHash with an enum_accessor set" do
152 | describe "#save" do
153 | it "resets the constant's value to the updated record" do
154 | expect(Borough::BROOKLYN.population).to eq(2556598)
155 | brooklyn = Borough.find_by_name("Brooklyn")
156 | brooklyn.population = 2556600
157 | expect(brooklyn.save).to be_truthy
158 | expect(Borough::BROOKLYN.population).to eq(2556600)
159 | end
160 | end
161 |
162 | describe ".create" do
163 | it "creates constants for new records" do
164 | bronx = Borough.create!(:name => "Bronx")
165 | expect(Borough::BRONX).to eq(bronx)
166 | end
167 |
168 | it "doesn't create constants for records missing the enum accessor field" do
169 | expect(Borough.create(:name => "")).to be_truthy
170 | expect(Borough.create(:population => 12)).to be_truthy
171 | end
172 | end
173 |
174 | describe ".delete_all" do
175 | it "unsets all constants for deleted records" do
176 | expect(Borough.const_defined?("STATEN_ISLAND")).to be_truthy
177 | expect(Borough.delete_all).to be_truthy
178 | expect(Borough.const_defined?("STATEN_ISLAND")).to be_falsey
179 | end
180 | end
181 | end
182 | end
183 |
--------------------------------------------------------------------------------
/spec/fixtures/array_products.yml:
--------------------------------------------------------------------------------
1 | - /aliases:
2 | soda_flavor: &soda_flavor
3 | sweet
4 | soda_price: &soda_price
5 | 1.0
6 | chip_flavor: &chip_flavor
7 | salty
8 | chip_price: &chip_price
9 | 1.5
10 |
11 | - id: 1
12 | name: Coke
13 | flavor: *soda_flavor
14 | price: *soda_price
15 |
16 | - id: 2
17 | name: Pepsi
18 | flavor: *soda_flavor
19 | price: *soda_price
20 |
21 | - id: 3
22 | name: Pringles
23 | flavor: *chip_flavor
24 | price: *chip_price
25 |
26 | - id: 4
27 | name: ETA
28 | flavor: *chip_flavor
29 | price: *chip_price
--------------------------------------------------------------------------------
/spec/fixtures/array_products_2.yml:
--------------------------------------------------------------------------------
1 | - /aliases:
2 | soda_flavor: &soda_flavor
3 | bitter
4 | soda_price: &soda_price
5 | 1.5
6 |
7 | - id: 5
8 | name: Schweppes
9 | flavor: *soda_flavor
10 | price: *soda_price
11 |
--------------------------------------------------------------------------------
/spec/fixtures/array_rows.json:
--------------------------------------------------------------------------------
1 | [
2 | {"id": 1,
3 | "name": "Row 1"},
4 | {"id": 2,
5 | "name": "Row 2"}
6 | ]
7 |
--------------------------------------------------------------------------------
/spec/fixtures/array_rows.yml:
--------------------------------------------------------------------------------
1 | - id: 1
2 | name: Row 1
3 | - id: 2
4 | name: Row 2
5 |
--------------------------------------------------------------------------------
/spec/fixtures/boroughs.yml:
--------------------------------------------------------------------------------
1 | - id: 1
2 | name: Manhattan
3 | county: New York
4 | population: 1634795
5 | - id: 2
6 | name: Brooklyn
7 | county: Kings
8 | population: 2556598
9 | - id: 3
10 | name: Queens
11 | county: Queens
12 | population: 2293007
13 | - id: 4
14 | name: Staten Island
15 | county: Richmond
16 | population: 487407
--------------------------------------------------------------------------------
/spec/fixtures/cities.json:
--------------------------------------------------------------------------------
1 | [
2 | {"id": 1,
3 | "state": "New York",
4 | "name": "Albany"},
5 | {"id": 2,
6 | "state": "Oregon",
7 | "name": "Portland"}
8 | ]
9 |
--------------------------------------------------------------------------------
/spec/fixtures/cities.yml:
--------------------------------------------------------------------------------
1 | albany:
2 | id: 1
3 | state: :new_york
4 | name: Albany
5 | portland:
6 | id: 2
7 | state: Oregon
8 | name: Portland
9 | melbourne:
10 | id: 3
11 | state: Vic
12 | name: Melbourne
13 | key: livable
14 |
--------------------------------------------------------------------------------
/spec/fixtures/commonwealths.json:
--------------------------------------------------------------------------------
1 | [
2 | {"name": "Puerto Rico"},
3 | {"name": "Phillipines"}
4 | ]
5 |
--------------------------------------------------------------------------------
/spec/fixtures/commonwealths.yml:
--------------------------------------------------------------------------------
1 | - name: Puerto Rico
2 | independence_date:
3 | created_at: Wed Jul 22 22:41:44 -0400 2009
4 | - name: Phillipines
5 | independence_date: 1946-07-04
6 | created_at: Wed Jul 22 22:41:44 -0400 2009
7 |
--------------------------------------------------------------------------------
/spec/fixtures/countries.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": 1,
4 | "name": "US",
5 | "custom_field_1": "value1"
6 | },
7 | {
8 | "id": 2,
9 | "name": "Canada",
10 | "custom_field_2": "value2"
11 | },
12 | {
13 | "id": 3,
14 | "name": "Mexico",
15 | "custom_field_3": "value3"
16 | }
17 | ]
18 |
--------------------------------------------------------------------------------
/spec/fixtures/countries.yml:
--------------------------------------------------------------------------------
1 | - id: 1
2 | name: US
3 | independence_date: 1776-07-04
4 | created_at: Wed Jul 22 22:41:44 -0400 2009
5 | custom_field_1: value1
6 | - id: 2
7 | name: Canada
8 | independence_date: 1867-07-01
9 | created_at: Wed Jul 22 22:41:44 -0400 2009
10 | custom_field_2: value2
11 | - id: 3
12 | name: Mexico
13 | independence_date: 1810-09-16
14 | created_at: Wed Jul 22 22:41:44 -0400 2009
15 | custom_field_3: value3
16 |
--------------------------------------------------------------------------------
/spec/fixtures/empties.json:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/active-hash/active_hash/2387d9c9e352f73e610c4c224a78b700b8ffb4d7/spec/fixtures/empties.json
--------------------------------------------------------------------------------
/spec/fixtures/empties.yml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/active-hash/active_hash/2387d9c9e352f73e610c4c224a78b700b8ffb4d7/spec/fixtures/empties.yml
--------------------------------------------------------------------------------
/spec/fixtures/key_products.yml:
--------------------------------------------------------------------------------
1 | /aliases:
2 | soda_flavor: &soda_flavor
3 | sweet
4 | soda_price: &soda_price
5 | 1.0
6 | chip_flavor: &chip_flavor
7 | salty
8 | chip_price: &chip_price
9 | 1.5
10 |
11 |
12 | coke:
13 | id: 1
14 | name: Coke
15 | flavor: *soda_flavor
16 | price: *soda_price
17 |
18 | pepsi:
19 | id: 2
20 | name: Pepsi
21 | flavor: *soda_flavor
22 | price: *soda_price
23 |
24 | pringles:
25 | id: 3
26 | name: Pringles
27 | flavor: *chip_flavor
28 | price: *chip_price
29 | slogan: &inner_alias
30 |
31 | eta:
32 | id: 4
33 | name: ETA
34 | flavor: *chip_flavor
35 | price: *chip_price
36 |
--------------------------------------------------------------------------------
/spec/fixtures/locales/ja.yml:
--------------------------------------------------------------------------------
1 | ja:
2 | activemodel:
3 | models:
4 | country: "国"
5 |
--------------------------------------------------------------------------------
/spec/fixtures/provinces.json:
--------------------------------------------------------------------------------
1 | {
2 | "quebec": {
3 | "id": "3",
4 | "name": "Quebec" },
5 | "british_columbia": {
6 | "id": "4",
7 | "name": "British Colombia"}
8 | }
9 |
--------------------------------------------------------------------------------
/spec/fixtures/provinces.yml:
--------------------------------------------------------------------------------
1 | quebec:
2 | id: 3
3 | name: Quebec
4 | british_columbia:
5 | id: 4
6 | name: British Colombia
7 |
--------------------------------------------------------------------------------
/spec/fixtures/states.json:
--------------------------------------------------------------------------------
1 | {
2 | "new_york":
3 | {"id": 1,
4 | "name": "New York"},
5 | "oregon":
6 | {"id": 2,
7 | "name": "Oregon"}
8 | }
9 |
--------------------------------------------------------------------------------
/spec/fixtures/states.yml:
--------------------------------------------------------------------------------
1 | new_york:
2 | id: 1
3 | name: New York
4 | oregon:
5 | id: 2
6 | name: Oregon
7 |
--------------------------------------------------------------------------------
/spec/fixtures/users.yml:
--------------------------------------------------------------------------------
1 | - id: 1
2 | email: <%= "user#{rand(100)}@email.com" %>
3 | password: <%= ENV['USER_PASSWORD'] %>
4 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require "bundler/setup"
2 | require "pry"
3 | require 'rspec'
4 | require 'yaml'
5 |
6 | SKIP_ACTIVE_RECORD = ENV['SKIP_ACTIVE_RECORD']
7 |
8 | $LOAD_PATH.unshift(File.dirname(__FILE__))
9 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
10 | require 'active_hash'
11 | require 'active_record' unless SKIP_ACTIVE_RECORD
12 |
13 | Dir["spec/support/**/*.rb"].each { |f|
14 | require File.expand_path(f)
15 | }
16 |
17 | if !SKIP_ACTIVE_RECORD && ActiveRecord::VERSION::MAJOR < 7
18 | RSpec.configure do |config|
19 | config.after(:each) do
20 | # To isolate tests with temporary classes.
21 | # ref: https://groups.google.com/g/rspec/c/7CQq0ABS3yQ
22 | ActiveSupport::Dependencies::Reference.clear!
23 | end
24 | end
25 | end
26 |
27 | I18n.load_path << File.expand_path("fixtures/locales/ja.yml", __dir__)
28 |
--------------------------------------------------------------------------------