├── .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 | [![Build Status](https://github.com/active-hash/active_hash/actions/workflows/ruby.yml/badge.svg)](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 | --------------------------------------------------------------------------------