├── .autotest ├── .gitignore ├── CHANGELOG ├── FAQ ├── MIT-LICENSE ├── PERFORMANCE ├── QUICKLINKS ├── README ├── Rakefile ├── TODO ├── lib ├── data_mapper.rb └── data_mapper │ ├── adapters.rb │ ├── adapters │ ├── abstract_adapter.rb │ ├── data_objects_adapter.rb │ ├── mysql_adapter.rb │ ├── postgres_adapter.rb │ └── sqlite3_adapter.rb │ ├── associations.rb │ ├── associations │ ├── many_to_many.rb │ ├── many_to_one.rb │ ├── one_to_many.rb │ ├── one_to_one.rb │ └── relationship.rb │ ├── hook.rb │ ├── identity_map.rb │ ├── loaded_set.rb │ ├── logger.rb │ ├── naming_conventions.rb │ ├── property.rb │ ├── property_set.rb │ ├── query.rb │ ├── repository.rb │ ├── resource.rb │ ├── scope.rb │ ├── support.rb │ ├── support │ ├── blank.rb │ ├── errors.rb │ ├── inflection.rb │ ├── kernel.rb │ ├── object.rb │ ├── pathname.rb │ ├── string.rb │ ├── struct.rb │ └── symbol.rb │ ├── type.rb │ ├── types.rb │ └── types │ ├── csv.rb │ ├── enum.rb │ ├── flag.rb │ ├── text.rb │ └── yaml.rb ├── script ├── all ├── performance.rb └── profile.rb └── spec ├── integration ├── association_spec.rb ├── data_objects_adapter_spec.rb ├── mysql_adapter_spec.rb ├── postgres_adapter_spec.rb ├── property_spec.rb ├── query_spec.rb ├── repository_spec.rb ├── sqlite3_adapter_spec.rb └── type_spec.rb ├── lib └── mock_adapter.rb ├── spec.opts ├── spec_helper.rb └── unit ├── adapters ├── abstract_adapter_spec.rb ├── adapter_shared_spec.rb └── data_objects_adapter_spec.rb ├── associations ├── many_to_many_spec.rb ├── many_to_one_spec.rb ├── one_to_many_spec.rb ├── one_to_one_spec.rb └── relationship_spec.rb ├── associations_spec.rb ├── hook_spec.rb ├── identity_map_spec.rb ├── loaded_set_spec.rb ├── naming_conventions_spec.rb ├── property_set_spec.rb ├── property_spec.rb ├── query_spec.rb ├── repository_spec.rb ├── resource_spec.rb ├── scope_spec.rb ├── support ├── blank_spec.rb ├── inflection_spec.rb ├── object_spec.rb ├── string_spec.rb └── struct_spec.rb ├── type_spec.rb └── types ├── enum_spec.rb └── flag_spec.rb /.autotest: -------------------------------------------------------------------------------- 1 | Autotest.add_hook :initialize do |at| 2 | ignore = %w[ .git burn www log plugins script tasks bin CHANGELOG FAQ MIT-LICENSE PERFORMANCE QUICKLINKS README ] 3 | 4 | unless ENV['AUTOTEST'] == 'integration' 5 | ignore << 'spec/integration' 6 | end 7 | 8 | ignore.each do |exception| 9 | at.add_exception(exception) 10 | end 11 | 12 | at.clear_mappings 13 | 14 | at.add_mapping(%r{^spec/.+_spec\.rb$}) do |filename,_| 15 | filename 16 | end 17 | 18 | at.add_mapping(%r{^lib/data_mapper/(.+)\.rb$}) do |_,match| 19 | [ "spec/unit/#{match[1]}_spec.rb" ] + 20 | at.files_matching(%r{^spec/integration/.+_spec\.rb$}) 21 | end 22 | 23 | at.add_mapping(%r{^spec/spec_helper\.rb$}) do 24 | at.files_matching(%r{^spec/.+_spec\.rb$}) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | log/* 3 | doc 4 | cov 5 | pkg 6 | .DS_Store 7 | www/output 8 | coverage/* 9 | *.db 10 | spec/integration/*.db* 11 | nbproject 12 | profile_results.* 13 | \#* 14 | TAGS -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | -- 0.1.0 2 | * Initial Public Release 3 | 4 | -- 0.1.1 5 | * Removed /lib/data_mapper/extensions 6 | * Moved ActiveRecordImpersonation into DataMapper::Support module 7 | * Moved CallbackHelper methods into DataMapper::Base class 8 | * Moved ValidationHelper into DataMapper::Validations module 9 | * Removed LoadedSet since it's not necessary for it to reference the Database, so it's nothing more than an array now; Replaced with Array 10 | * Modified data_mapper.rb to load DataMapper::Support::Enumerable 11 | * Modified example.rb and performance.rb to require 'lib/data_mapper' instead of modifying $LOADPATH 12 | * Created SqlAdapter base-class 13 | * Refactored MysqlAdapter to use SqlAdapter superclass 14 | * Refactored Sqlite3Adapter to use SqlAdapter superclass 15 | * Moved /lib/data_mapper/queries to /lib/data_mapper/adapters/sql/queries 16 | * Moved Connection, Result and Reader classes along with Coersion and Quoting modules to DataMapper::Adapters::Sql module 17 | * Moved DataMapper::Adapters::Sql::Queries to ::Commands 18 | * Moved Mappings to SqlAdapter 19 | * Added monolithic DeleteCommand 20 | * Added monolithic SaveCommand 21 | * Added TableExistsCommand 22 | * Moved save/delete logic out of Session 23 | * Added create-table functionality to SaveCommand 24 | * Cleaned up Session; #find no longer supported, use #all or #first 25 | * Moved object materialization into LoadCommand 26 | * Migrated Sqlite3Adapter::Commands 27 | * Added Session#query support back in 28 | * Removed Connection/Reader/Result classes 29 | * Set DataMapper::Base#key on load to avoid double-hit against Schema 30 | * Added DataMapper::Support::Struct for increased Session#query performance 31 | * Added AdvancedHasManyAssociation (preview status) 32 | * Added benchmarks comparing ActiveRecord::Base::find_by_sql with Session#query 33 | 34 | -- 0.2.0 35 | * AdvancedHasManyAssociation now functional for fetches 36 | * AdvancedHasManyAssociation renamed to HasNAssociation 37 | * HasManyAssociation refactored to use HasNAssociation superclass 38 | * Slight spec tweaks to accomodate the updates 39 | * HasOneAssociation refactored to use HasNAssociation superclass 40 | * Added HasAndBelongsToManyAssociation, using HasNAssociation as a basis; Need to add corresponding SQL generation code in AdvancedLoadCommand 41 | * Added spec for habtm query generation 42 | * HasNAssociation#foreign_key returns a DataMapper::Adapters::Sql::Mappings::Column instance instead of a raw String now 43 | * Added table, association, association_table and to_sql methods to HasNAssociation 44 | * Added associations_spec.rb 45 | * Added a forced table-recreation to spec_helper.rb so the tests could run with a clean version of the database, including any new columns added to the models 46 | * Added HasAndBelongsToManyAssociation#to_sql (all current specs pass now!) 47 | * Minor tweaks to Callbacks 48 | * Added CallbacksHelper to declare class-method ::callbacks on DataMapper::Base 49 | * Implemented before_validate and after_validate hooks in ValidationHelper 50 | * Minor documentation additions in callbacks.rb 51 | * Added callbacks_spec 52 | * Moved class-method declarations for built-in callbacks to the callbacks helper instead of DataMapper::Base 53 | * Renamed :before/after_validate callback to :before/after_validation to match ActiveRecord 54 | * Callbacks#add now accepts a Symbol which maps a callback to a method call on the targetted instance, also added a spec to verify this behavior 55 | * Documented callbacks.rb 56 | * Added DataMapper::Associations::Reference class 57 | * Documented DataMapper::Associations::Reference class 58 | * Upgraded BelongsToAssociation to new style 59 | * Added AssociationsSet to handle simple "last-in" for association bindings 60 | * Fixed extra spec loading 61 | * Added *Association#columns 62 | * Some refactoring in AdvancedLoadCommand regarding :include options 63 | * Added support for class-less Mappings::Table instances, with just a string name 64 | * HasAndBelongsToManyAssociation#join_table #left_foreign_key and #right_foreign_key reference actual Table or Column objects now 65 | * Added :shallow_include option for HABTM joins in AdvancedLoadCommand and corresponding spec 66 | * Added Commands::AdvancedConditions 67 | * Added ORDER, LIMIT, OFFSET and WHERE support to AdvancedLoadCommand 68 | * Renamed spec/has_many.rb to spec/has_many_spec.rb 69 | * Tweaked the loading of has_many relationships; big performance boost; got rid of an extra query 70 | * Added EmbeddedValue support, and accompanying spec 71 | * Fleshed out AdvancedConditions a bit; added conditions_spec.rb 72 | * Added more AdvancedConditions specs 73 | * Added Loader to handle multi-instanced rows 74 | * AdvancedLoadCommand replaced LoadCommand; down to 3 failing specs 75 | * All specs pass 76 | * Added :intercept_load finder option and accompanying spec 77 | * Modified :intercept_load block signature to |instance,columns,row| 78 | * HasAndBelongsToMany works, all specs pass 79 | * Fixed a couple bugs with keys; Added DataMapper::Base#key= method 80 | * Made DataMapper::Base#lazy_load! a little more flexible 81 | * Removed LoadCommand overwrites from MysqlAdapter 82 | * Default Database#single_threaded mode is true now 83 | * Removed MysqlAdapter#initialize, which only served to setup the connections, moved to SqlAdapter 84 | * Added SqlAdapter#create_connection and SqlAdapter#close_connection abstract methods 85 | * Added MysqlAdapter#create_connection and MysqlAdapter#close_connection concrete methods 86 | * Made SqlAdapter#connection a concrete method (instead of abstract), with support for single_threaded operation 87 | * Database#setup now takes a Hash of options instead of a block-initializer 88 | * Validation chaining should work for all association types 89 | * Save chaining should work for has_many associations 90 | * Added benchmarks for in-session performance to performance.rb 91 | * Removed block conditions; They're slower and don't offer any real advantages 92 | * Removed DeleteCommand 93 | * Removed SaveCommand 94 | * Removed TableExistsCommand 95 | * Session renamed to Context 96 | * Most command implementations moved to methods in SqlAdapter 97 | * Removed UnitOfWork module, instead moving a slightly refactored implementation into Base 98 | 99 | -- 0.2.1 100 | * Added :float column support 101 | * Added association proxies: ie: Zoo.first.exhibits.animals 102 | * Columns stored in SortedSet 103 | * Swig files are no longer RDOCed 104 | * Added :date column support 105 | * BUG: Fixed UTC issues with datetimes 106 | * Added #to_yaml method 107 | * Added #to_xml method 108 | * Added #to_json method 109 | * BUG: Fixed HasManyAssociation::Set#inspect 110 | * BUG: Fixed #reload! 111 | * BUG: Column copy for STI moved into Table#initialize to better handle STI with multiple mapped databases 112 | * BUG: before_create callbacks moved in the execution flow since they weren't guaranteed to fire before 113 | * Threading enhancements: Removed single_threaded_mode, #database block form adjusted for thread-safety 114 | * BUG: Fixed String#blank? when a multi-line string contained a blank line (thanks zapnap!) 115 | * Performance enhancements: (thanks wycats!) 116 | 117 | -- 0.2.2 118 | * Removed C extension bundles and log files from package 119 | 120 | -- 0.2.3 121 | * Added String#t for translation and overrides for default validation messages 122 | * Give credit where it's due: zapnap, not pimpmaster, submitted the String#blank? patch. My bad. :-( 123 | * MAJOR: Resolve issue with non-unique-hash values and #dirty?; now frozen original values are stored instead 124 | * Added Base#update_attributes 125 | * MAJOR: Queries are now passed to the database drivers in a parameterized fashion 126 | * Updated PostgreSQL driver and adapter to current 127 | 128 | -- 0.2.4 129 | * Bug fixes 130 | * Added paranoia 131 | 132 | -- 0.2.5 133 | * has_one bugfixes 134 | * Added syntax for setting CHECK-constraints directly in your properties (Postgres) 135 | * You can now set indexes with :index => true and :index => :unique 136 | * Support for composite indexes (thanks to Jeffrey Gelens) 137 | * Add composite scope to validates_uniqueness 138 | * Added private/protected properties 139 | * Remove HasOneAssociation, Make HasManyAssociation impersonate has_one relationships 140 | * Added #get method 141 | * Persistence module added, inheriting from DataMapper::Base no longer necessary 142 | 143 | -- 0.3.0 144 | * HasManyAssociation::Set now has a nil? method, so we can do stuff like cage.animal.nil? 145 | 146 | -------------------------------------------------------------------------------- /FAQ: -------------------------------------------------------------------------------- 1 | :include:QUICKLINKS 2 | 3 | = FAQ 4 | 5 | === I don't want to use :id as a primary key, but I don't see 6 | === set_primary_key anywhere. What do I do? 7 | 8 | If you're working with a table that doesn't have a :id column, you 9 | can declare your properties as you usually do, and declare one of them as a 10 | natural key. 11 | 12 | property :name, String, :key => true 13 | 14 | You should now be able to do Class['name_string'] as well. Remember: 15 | this column should be unique, so treat it that way. This is the equivalent to 16 | using set_primary_key in ActiveRecord. 17 | 18 | 19 | === How do I make a model paranoid? 20 | 21 | property :deleted_at, DateTime 22 | 23 | If you've got deleted_at, your model is paranoid auto-magically. All of your 24 | calls to ##all(), ##first(), and ##count() will be 25 | scoped with where deleted_at is null. Plus, you won't see deleted 26 | objects in your associations. 27 | 28 | === Does DataMapper support Has Many Through? 29 | 30 | Write me! 31 | 32 | === What about Self-Referential Has And Belongs to Many? 33 | 34 | Sure does. Here's an example implementation: 35 | 36 | class Task 37 | include DataMapper::Resource 38 | many_to_many :tasks, 39 | :join_table => "task_relationships", :left_foreign_key => "parent_id", 40 | :right_foreign_key => "child_id" 41 | end 42 | 43 | You'll notice that instead of foreign_key and 44 | association_foreign_key, DataMapper uses the "database-y" terms 45 | left_foreign_key, and right_foreign_key. 46 | 47 | === Does DataMapper do Single Table Inheritance? 48 | 49 | Oh yes, and particularly well too. 50 | 51 | class Person 52 | include DataMapper::Resource 53 | property :type, Class ## other shared properties here 54 | end 55 | 56 | class Salesperson < Person; end 57 | 58 | You can claim a column to have the type :class and DataMapper will 59 | automatically drop the class name of the inherited classes into that column of 60 | the database. 61 | 62 | === What about Class Table Inheritance? 63 | 64 | Class Table Inheritance is on the drawing board and everyone's drooling over 65 | it. So no, not yet, but soon. 66 | 67 | === How do I run my own commands? 68 | 69 | You're probably asking for find_by_sql, and DataMapper has that in 70 | it's ActiveRecordImpersonation, but if you want to go straight-up DataMapper, 71 | you'll want to use repository.query 72 | 73 | repository.query("select * from users where clue > 0") 74 | 75 | This does not return any Users (har har), but rather Struct's that will quack 76 | like Users. They'll be read-only as well. 77 | 78 | repository.query shouldn't be used if you aren't expecting a result set 79 | back. If you want to just execute something against the database, use 80 | repository.execute instead. 81 | 82 | === Can I batch-process a ton of records at once? 83 | 84 | User.each(:performance_rating => "low") do |u| 85 | u.employment_status = "fired" 86 | u.save 87 | end 88 | 89 | With ActiveRecord, doing a User.find(:all).each{} would execute the 90 | find, instantiate an object for EVERY result, THEN apply your transformations 91 | to each object in turn. Doesn't sound too horrible unless you have a TON of 92 | records; you WILL grind your system to a screeching and bloody halt. 93 | 94 | DataMapper's #each works in sets of 500 so the amount of objects 95 | instantiated at a time won't make your computer think it's a victim in a Saw 96 | movie. Once it's done executing your block on the first set of 500, it moves 97 | on to the next. 98 | 99 | What's more is #each is secretly a finder too. You can pass it an 100 | options hash and it'll only iterate on 500-item sets matching your query. 101 | Don't send it :offset though, because that's how it pages. You can 102 | overload the page size by sending it :limit 103 | 104 | === Can I get an SQL log of what queries DataMapper is issuing? 105 | 106 | Yup, when you issue Repository.setup, tack on the log_stream 107 | and log_level: 108 | 109 | DataMapper::Repository.setup({ 110 | :adapter => 'mysql', :host => 'localhost', :username => 'root', 111 | :password => 'R00tPaswooooord', :database => 112 | 'myspiffyblog_development', :log_stream => 'log/sql.log', :log_level => 0 113 | }) 114 | 115 | By supplying the log_stream you're telling DataMapper what file you 116 | want to see your sql logs in. log_level is the 117 | Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc/] level of output you 118 | want to see there. 0, in this case, says that you want to see all DEBUG level 119 | messages (and higher) sent to the logger. For more information on how to work 120 | with Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc/], hit up 121 | http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc/. 122 | 123 | Incidentally, if you'd like to send a message into the DataMapper logger, do: 124 | 125 | repository.adapter.logger.info "your message here" 126 | 127 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007 Sam Smoot 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /QUICKLINKS: -------------------------------------------------------------------------------- 1 | = Quick Links 2 | 3 | * Finders and CRUD - DataMapper::Persistence::ConvenienceMethods::ClassMethods 4 | * Properties - DataMapper::Property 5 | * Validations - Validatable 6 | * Migrations 7 | * FAQ[link:/files/FAQ.html] 8 | * Contact Us 9 | * Website - http://www.datamapper.org 10 | * Bug Reports - http://wm.lighthouseapp.com/projects/4819-datamapper/overview 11 | * IRC Channel - ##datamapper on irc.freenode.net 12 | * Mailing List - http://groups.google.com/group/datamapper/ -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | 2 | :include:QUICKLINKS 3 | 4 | = Why DataMapper? 5 | 6 | == Open Development 7 | 8 | DataMapper sports a very accessible code-base and a welcoming community. 9 | Outside contributions and feedback are welcome and encouraged, especially 10 | constructive criticism. Make your voice heard! Submit a 11 | ticket[http://wm.lighthouseapp.com/projects/4819-datamapper/overview] or 12 | patch[http://wm.lighthouseapp.com/projects/4819-datamapper/overview], speak up 13 | on our mailing-list[http://groups.google.com/group/datamapper/], chat with us 14 | on irc[irc://irc.freenode.net/#datamapper], write a spec, get it reviewed, ask 15 | for commit rights. It's as easy as that to become a contributor. 16 | 17 | == Identity Map 18 | 19 | One row in the database should equal one object reference. Pretty simple idea. 20 | Pretty profound impact. If you run the following code in ActiveRecord you'll 21 | see all false results. Do the same in DataMapper and it's 22 | true all the way down. 23 | 24 | @parent = Tree.find(:first, :conditions => ['name = ?', 'bob']) 25 | 26 | @parent.children.each do |child| 27 | puts @parent.object_id == child.parent.object_id 28 | end 29 | 30 | This makes DataMapper faster and allocate less resources to get things done. 31 | 32 | == Don't Do What You Don't Have To 33 | 34 | ActiveRecord updates every column in a row during a save whether that column 35 | changed or not. So it performs work it doesn't really need to making it much 36 | slower, and more likely to eat data during concurrent access if you don't go 37 | around adding locking support to everything. 38 | 39 | DataMapper only does what it needs to. So it plays well with others. You can 40 | use it in an Integration Database without worrying that your application will 41 | be a bad actor causing trouble for all of your other processes. 42 | 43 | == Eager Loading 44 | 45 | Ready for something amazing? The following example executes only two queries. 46 | 47 | zoos = Zoo.all 48 | first = zoos.first 49 | first.exhibits # Loads the exhibits for all the Zoo objects in the zoos variable. 50 | 51 | Pretty impressive huh? The idea is that you aren't going to load a set of 52 | objects and use only an association in just one of them. This should hold up 53 | pretty well against a 99% rule. When you don't want it to work like this, just 54 | load the item you want in it's own set. So the DataMapper thinks ahead. We 55 | like to call it "performant by default". This feature single-handedly wipes 56 | out the "N+1 Query Problem". No need to specify an include option in 57 | your finders. 58 | 59 | == Laziness Can Be A Virtue 60 | 61 | Text columns are expensive in databases. They're generally stored in a 62 | different place than the rest of your data. So instead of a fast sequential 63 | read from your hard-drive, your database server has to hop around all over the 64 | place to get what it needs. Since ActiveRecord returns everything by default, 65 | adding a text column to a table slows everything down drastically, across the 66 | board. 67 | 68 | Not so with the DataMapper. Text fields are treated like in-row associations 69 | by default, meaning they only load when you need them. If you want more 70 | control you can enable or disable this feature for any column (not just 71 | text-fields) by passing a @lazy@ option to your column mapping with a value of 72 | true or false. 73 | 74 | class Animal 75 | include DataMapper::Resource 76 | property :name, String 77 | property :notes, DataMapper::Types::Text, :lazy => false 78 | end 79 | 80 | Plus, lazy-loading of text fields happens automatically and intelligently when 81 | working with associations. The following only issues 2 queries to load up all 82 | of the notes fields on each animal: 83 | 84 | animals = Animal.all 85 | animals.each do |pet| 86 | pet.notes 87 | end 88 | 89 | == Plays Well With Others 90 | 91 | In ActiveRecord, all your columns are mapped, whether you want them or not. 92 | This slows things down. In the DataMapper you define your mappings in your 93 | model. So instead of an _ALTER TABLE ADD COLUMN_ in your Database, you simply 94 | add a property :name, :string to your model. DRY. No schema.rb. No 95 | migration files to conflict or die without reverting changes. Your model 96 | drives the database, not the other way around. 97 | 98 | Unless of course you want to map to a legacy database. Raise your hand if you 99 | like seeing a method called col2Name on your model just because 100 | that's what it's called in an old database you can't afford to change right 101 | now? In DataMapper you control the mappings: 102 | 103 | class Fruit 104 | include DataMapper::Resource 105 | set_table_name 'frt' 106 | property :name, String, :column => 'col2Name' 107 | end 108 | 109 | == All Ruby, All The Time 110 | 111 | It's great that ActiveRecord allows you to write SQL when you need to, but 112 | should we have to so often? 113 | 114 | DataMapper supports issuing your own SQL, but it also provides more helpers 115 | and a unique hash-based condition syntax to cover more of the use-cases where 116 | issuing your own SQL would have been the only way to go. For example, any 117 | finder option that's non-standard is considered a condition. So you can write 118 | Zoo.all(:name => 'Dallas') and DataMapper will look for zoos with the 119 | name of 'Dallas'. 120 | 121 | It's just a little thing, but it's so much nicer than writing 122 | Zoo.find(:all, :conditions => ['name = ?', 'Dallas']). What if you 123 | need other comparisons though? Try these: 124 | 125 | Zoo.first(:name => 'Galveston') 126 | 127 | # 'gt' means greater-than. We also do 'lt'. 128 | Person.all(:age.gt => 30) 129 | 130 | # 'gte' means greather-than-or-equal-to. We also do 'lte'. 131 | Person.all(:age.gte => 30) 132 | 133 | Person.all(:name.not => 'bob') 134 | 135 | # If the value of a pair is an Array, we do an IN-clause for you. 136 | Person.all(:name.like => 'S%', :id => [1, 2, 3, 4, 5]) 137 | 138 | # An alias for Zoo.find(11) 139 | Zoo[11] 140 | 141 | # Does a NOT IN () clause for you. 142 | Person.all(:name.not => ['bob','rick','steve']) 143 | 144 | See? Fewer SQL fragments dirtying your Ruby code. And that's just a few of the 145 | nice syntax tweaks DataMapper delivers out of the box... 146 | 147 | == Better Is Great, But Familiar Is Nice 148 | 149 | The DataMapper also supports a lot of old-fashioned ActiveRecord syntax. We 150 | want to make it easy for you to get started, so aside from mapping your 151 | columns and changing the base-class your models inherit from, much of AR 152 | syntax for finders are supported as well, making your transition easy. 153 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'pathname' 4 | require 'rubygems' 5 | require 'rake' 6 | require Pathname('spec/rake/spectask') 7 | require Pathname('rake/rdoctask') 8 | require Pathname('rake/gempackagetask') 9 | require Pathname('rake/contrib/rubyforgepublisher') 10 | 11 | ROOT = Pathname(__FILE__).dirname.expand_path 12 | 13 | Pathname.glob(ROOT + 'tasks/**/*.rb') { |t| require t } 14 | 15 | task :default => 'dm:spec' 16 | task :spec => 'dm:spec' 17 | 18 | namespace :spec do 19 | task :unit => 'dm:spec:unit' 20 | task :integration => 'dm:spec:integration' 21 | end 22 | 23 | desc 'Remove all package, rdocs and spec products' 24 | task :clobber_all => %w[ clobber_package clobber_rdoc dm:clobber_spec ] 25 | 26 | 27 | 28 | namespace :dm do 29 | def run_spec(name, files, rcov = true) 30 | Spec::Rake::SpecTask.new(name) do |t| 31 | t.spec_opts << '--format' << 'specdoc' << '--colour' 32 | t.spec_opts << '--loadby' << 'random' 33 | t.spec_files = Pathname.glob(ENV['FILES'] || files) 34 | t.rcov = ENV.has_key?('NO_RCOV') ? ENV['NO_RCOV'] != 'true' : rcov 35 | t.rcov_opts << '--exclude' << 'spec,environment.rb' 36 | t.rcov_opts << '--text-summary' 37 | t.rcov_opts << '--sort' << 'coverage' << '--sort-reverse' 38 | t.rcov_opts << '--only-uncovered' 39 | end 40 | end 41 | 42 | desc "Run all specifications" 43 | run_spec('spec', ROOT + 'spec/**/*_spec.rb') 44 | 45 | namespace :spec do 46 | desc "Run unit specifications" 47 | run_spec('unit', ROOT + 'spec/unit/**/*_spec.rb', false) 48 | 49 | desc "Run integration specifications" 50 | run_spec('integration', ROOT + 'spec/integration/**/*_spec.rb', false) 51 | end 52 | 53 | desc "Run comparison with ActiveRecord" 54 | task :perf do 55 | load Pathname.glob(ROOT + 'script/performance.rb') 56 | end 57 | 58 | desc "Profile DataMapper" 59 | task :profile do 60 | load Pathname.glob(ROOT + 'script/profile.rb') 61 | end 62 | end 63 | 64 | PACKAGE_VERSION = '0.9.0' 65 | 66 | PACKAGE_FILES = [ 67 | 'README', 68 | 'FAQ', 69 | 'QUICKLINKS', 70 | 'CHANGELOG', 71 | 'MIT-LICENSE', 72 | '*.rb', 73 | 'lib/**/*.rb', 74 | 'spec/**/*.{rb,yaml}', 75 | 'tasks/**/*', 76 | 'plugins/**/*' 77 | ].collect { |pattern| Pathname.glob(pattern) }.flatten.reject { |path| path.to_s =~ /(\/db|Makefile|\.bundle|\.log|\.o)\z/ } 78 | 79 | DOCUMENTED_FILES = PACKAGE_FILES.reject do |path| 80 | path.directory? || path.to_s.match(/(?:^spec|\/spec|\/swig\_)/) 81 | end 82 | 83 | PROJECT = "dm-core" 84 | 85 | desc 'List all package files' 86 | task :ls do 87 | puts PACKAGE_FILES 88 | end 89 | 90 | desc "Generate Documentation" 91 | rd = Rake::RDocTask.new do |rdoc| 92 | rdoc.rdoc_dir = 'doc' 93 | rdoc.title = "DataMapper -- An Object/Relational Mapper for Ruby" 94 | rdoc.options << '--line-numbers' << '--inline-source' << '--main' << 'README' 95 | rdoc.rdoc_files.include(*DOCUMENTED_FILES.map { |file| file.to_s }) 96 | end 97 | 98 | gem_spec = Gem::Specification.new do |s| 99 | s.platform = Gem::Platform::RUBY 100 | s.name = PROJECT 101 | s.summary = "An Object/Relational Mapper for Ruby" 102 | s.description = "Faster, Better, Simpler." 103 | s.version = PACKAGE_VERSION 104 | 105 | s.authors = "Sam Smoot" 106 | s.email = "ssmoot@gmail.com" 107 | s.rubyforge_project = PROJECT 108 | s.homepage = "http://datamapper.org" 109 | 110 | s.files = PACKAGE_FILES.map { |f| f.to_s } 111 | 112 | s.require_path = "lib" 113 | s.requirements << "none" 114 | s.add_dependency("data_objects", ">=0.9.0") 115 | s.add_dependency("english", ">=0.2.0") 116 | s.add_dependency("rspec", ">=1.1.3") 117 | 118 | s.has_rdoc = true 119 | s.rdoc_options << "--line-numbers" << "--inline-source" << "--main" << "README" 120 | s.extra_rdoc_files = DOCUMENTED_FILES.map { |f| f.to_s } 121 | end 122 | 123 | Rake::GemPackageTask.new(gem_spec) do |p| 124 | p.gem_spec = gem_spec 125 | p.need_tar = true 126 | p.need_zip = true 127 | end 128 | 129 | desc "Publish to RubyForge" 130 | task :rubyforge => [ :rdoc, :gem ] do 131 | Rake::SshDirPublisher.new("#{ENV['RUBYFORGE_USER']}@rubyforge.org", "/var/www/gforge-projects/#{PROJECT}", 'doc').upload 132 | end 133 | 134 | desc "Install #{PROJECT}" 135 | task :install => :package do 136 | sh %{sudo gem install pkg/#{PROJECT}-#{PACKAGE_VERSION}} 137 | end 138 | 139 | if RUBY_PLATFORM.match(/mswin32|cygwin|mingw|bccwin/) 140 | namespace :dev do 141 | desc 'Install for development (for windows)' 142 | task :winstall => :gem do 143 | system %{gem install --no-rdoc --no-ri -l pkg/#{PROJECT}-#{PACKAGE_VERSION}.gem} 144 | end 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | See: http://wiki.datamapper.org/doku.php?id=what_needs_to_be_done -------------------------------------------------------------------------------- /lib/data_mapper.rb: -------------------------------------------------------------------------------- 1 | # This file begins the loading sequence. 2 | # 3 | # Quick Overview: 4 | # * Requires set, fastthread, support libs, and base 5 | # * Sets the applications root and environment for compatibility with rails or merb 6 | # * Checks for the database.yml and loads it if it exists 7 | # * Sets up the database using the config from the yaml file or from the environment 8 | # 9 | 10 | # Require the basics... 11 | require 'date' 12 | require 'pathname' 13 | require 'rubygems' 14 | require 'set' 15 | require 'time' 16 | require 'uri' 17 | require 'yaml' 18 | 19 | begin 20 | require 'fastthread' 21 | rescue LoadError 22 | end 23 | 24 | # for Pathname / 25 | require File.expand_path(File.join(File.dirname(__FILE__), 'data_mapper', 'support', 'pathname')) 26 | 27 | dir = Pathname(__FILE__).dirname.expand_path / 'data_mapper' 28 | 29 | require dir / 'associations' 30 | require dir / 'hook' 31 | require dir / 'identity_map' 32 | require dir / 'loaded_set' 33 | require dir / 'logger' 34 | require dir / 'naming_conventions' 35 | require dir / 'property_set' 36 | require dir / 'query' 37 | require dir / 'repository' 38 | require dir / 'resource' 39 | require dir / 'scope' 40 | require dir / 'support' 41 | require dir / 'type' 42 | require dir / 'types' 43 | require dir / 'property' 44 | require dir / 'adapters' 45 | 46 | module DataMapper 47 | def self.root 48 | @root ||= Pathname(__FILE__).dirname.parent.expand_path 49 | end 50 | 51 | def self.setup(name, uri_or_options) 52 | raise ArgumentError, "+name+ must be a Symbol, but was #{name.class}", caller unless Symbol === name 53 | 54 | case uri_or_options 55 | when Hash 56 | adapter_name = uri_or_options[:adapter] 57 | when String, URI 58 | uri_or_options = URI.parse(uri_or_options) if String === uri_or_options 59 | adapter_name = uri_or_options.scheme 60 | else 61 | raise ArgumentError, "+uri_or_options+ must be a Hash, URI or String, but was #{uri_or_options.class}", caller 62 | end 63 | 64 | # TODO: use autoload to load the adapter on-the-fly when used 65 | class_name = DataMapper::Inflection.classify(adapter_name) + 'Adapter' 66 | 67 | unless Adapters::const_defined?(class_name) 68 | lib_name = "#{DataMapper::Inflection.underscore(adapter_name)}_adapter" 69 | begin 70 | require root / 'lib' / 'data_mapper' / 'adapters' / lib_name 71 | rescue LoadError 72 | require lib_name 73 | end 74 | end 75 | 76 | Repository.adapters[name] = Adapters::const_get(class_name).new(name, uri_or_options) 77 | end 78 | 79 | # ===Block Syntax: 80 | # Pushes the named repository onto the context-stack, 81 | # yields a new session, and pops the context-stack. 82 | # 83 | # results = DataMapper.repository(:second_database) do |current_context| 84 | # ... 85 | # end 86 | # 87 | # ===Non-Block Syntax: 88 | # Returns the current session, or if there is none, 89 | # a new Session. 90 | # 91 | # current_repository = DataMapper.repository 92 | def self.repository(name = nil) # :yields: current_context 93 | # TODO return context.last if last.name == name (arg) 94 | current_repository = if name 95 | Repository.new(name) 96 | else 97 | Repository.context.last || Repository.new(Repository.default_name) 98 | end 99 | 100 | return current_repository unless block_given? 101 | 102 | Repository.context << current_repository 103 | 104 | begin 105 | return yield(current_repository) 106 | ensure 107 | Repository.context.pop 108 | end 109 | end 110 | end 111 | -------------------------------------------------------------------------------- /lib/data_mapper/adapters.rb: -------------------------------------------------------------------------------- 1 | dir = Pathname(__FILE__).dirname.expand_path / 'adapters' 2 | 3 | require dir / 'abstract_adapter' 4 | require dir / 'data_objects_adapter' 5 | -------------------------------------------------------------------------------- /lib/data_mapper/adapters/mysql_adapter.rb: -------------------------------------------------------------------------------- 1 | gem 'do_mysql', '=0.9.0' 2 | require 'do_mysql' 3 | 4 | module DataMapper 5 | module Adapters 6 | 7 | # Options: 8 | # host, user, password, database (path), socket(uri query string), port 9 | class MysqlAdapter < DataObjectsAdapter 10 | 11 | def begin_transaction(transaction) 12 | cmd = "XA START '#{transaction_id(transaction)}'" 13 | transaction.connection_for(self).create_command(cmd).execute_non_query 14 | DataMapper.logger.debug("#{self}: #{cmd}") 15 | end 16 | 17 | def transaction_id(transaction) 18 | "#{transaction.id}:#{self.object_id}" 19 | end 20 | 21 | def commit_transaction(transaction) 22 | cmd = "XA COMMIT '#{transaction_id(transaction)}'" 23 | transaction.connection_for(self).create_command(cmd).execute_non_query 24 | DataMapper.logger.debug("#{self}: #{cmd}") 25 | end 26 | 27 | def finalize_transaction(transaction) 28 | cmd = "XA END '#{transaction_id(transaction)}'" 29 | transaction.connection_for(self).create_command(cmd).execute_non_query 30 | DataMapper.logger.debug("#{self}: #{cmd}") 31 | end 32 | 33 | def prepare_transaction(transaction) 34 | finalize_transaction(transaction) 35 | cmd = "XA PREPARE '#{transaction_id(transaction)}'" 36 | transaction.connection_for(self).create_command(cmd).execute_non_query 37 | DataMapper.logger.debug("#{self}: #{cmd}") 38 | end 39 | 40 | def rollback_transaction(transaction) 41 | finalize_transaction(transaction) 42 | cmd = "XA ROLLBACK '#{transaction_id(transaction)}'" 43 | transaction.connection_for(self).create_command(cmd).execute_non_query 44 | DataMapper.logger.debug("#{self}: #{cmd}") 45 | end 46 | 47 | def rollback_prepared_transaction(transaction) 48 | cmd = "XA ROLLBACK '#{transaction_id(transaction)}'" 49 | transaction.connection.create_command(cmd).execute_non_query 50 | DataMapper.logger.debug("#{self}: #{cmd}") 51 | end 52 | 53 | private 54 | 55 | def quote_table_name(table_name) 56 | "`#{table_name}`" 57 | end 58 | 59 | def quote_column_name(column_name) 60 | "`#{column_name}`" 61 | end 62 | 63 | def rewrite_uri(uri, options) 64 | new_uri = uri.dup 65 | new_uri.host = options[:host] || uri.host 66 | new_uri.user = options[:user] || uri.user 67 | new_uri.password = options[:password] || uri.password 68 | new_uri.path = (options[:database] && "/" << options[:database]) || uri.path 69 | new_uri.port = options[:port] || uri.port 70 | new_uri.query = (options[:socket] && "socket=#{options[:socket]}") || uri.query 71 | 72 | new_uri 73 | end 74 | 75 | end # class MysqlAdapter 76 | end # module Adapters 77 | end # module DataMapper 78 | -------------------------------------------------------------------------------- /lib/data_mapper/adapters/postgres_adapter.rb: -------------------------------------------------------------------------------- 1 | gem 'do_postgres', '=0.9.0' 2 | require 'do_postgres' 3 | 4 | module DataMapper 5 | module Adapters 6 | 7 | class PostgresAdapter < DataObjectsAdapter 8 | 9 | def begin_transaction(transaction) 10 | cmd = "BEGIN" 11 | transaction.connection_for(self).create_command(cmd).execute_non_query 12 | DataMapper.logger.debug("#{self}: #{cmd}") 13 | end 14 | 15 | def transaction_id(transaction) 16 | "#{transaction.id}:#{self.object_id}" 17 | end 18 | 19 | def commit_transaction(transaction) 20 | cmd = "COMMIT PREPARED '#{transaction_id(transaction)}'" 21 | transaction.connection_for(self).create_command(cmd).execute_non_query 22 | DataMapper.logger.debug("#{self}: #{cmd}") 23 | end 24 | 25 | def prepare_transaction(transaction) 26 | cmd = "PREPARE TRANSACTION '#{transaction_id(transaction)}'" 27 | transaction.connection_for(self).create_command(cmd).execute_non_query 28 | DataMapper.logger.debug("#{self}: #{cmd}") 29 | end 30 | 31 | def rollback_transaction(transaction) 32 | cmd = "ROLLBACK" 33 | transaction.connection_for(self).create_command(cmd).execute_non_query 34 | DataMapper.logger.debug("#{self}: #{cmd}") 35 | end 36 | 37 | def rollback_prepared_transaction(transaction) 38 | cmd = "ROLLBACK PREPARED '#{transaction_id(transaction)}'" 39 | transaction.connection.create_command(cmd).execute_non_query 40 | DataMapper.logger.debug("#{self}: #{cmd}") 41 | end 42 | 43 | def create_with_returning?; true; end 44 | 45 | end # class PostgresAdapter 46 | 47 | end # module Adapters 48 | end # module DataMapper 49 | -------------------------------------------------------------------------------- /lib/data_mapper/adapters/sqlite3_adapter.rb: -------------------------------------------------------------------------------- 1 | gem 'do_sqlite3', '=0.9.0' 2 | require 'do_sqlite3' 3 | 4 | module DataMapper 5 | module Adapters 6 | 7 | class Sqlite3Adapter < DataObjectsAdapter 8 | 9 | def begin_transaction(transaction) 10 | cmd = "BEGIN" 11 | transaction.connection_for(self).create_command(cmd).execute_non_query 12 | DataMapper.logger.debug("#{self}: #{cmd}") 13 | end 14 | 15 | def commit_transaction(transaction) 16 | cmd = "COMMIT" 17 | transaction.connection_for(self).create_command(cmd).execute_non_query 18 | DataMapper.logger.debug("#{self}: #{cmd}") 19 | end 20 | 21 | def prepare_transaction(transaction) 22 | DataMapper.logger.debug("#{self}: #prepare_transaction called, but I don't know how... I hope the commit comes pretty soon!") 23 | end 24 | 25 | def rollback_transaction(transaction) 26 | cmd = "ROLLBACK" 27 | transaction.connection_for(self).create_command(cmd).execute_non_query 28 | DataMapper.logger.debug("#{self}: #{cmd}") 29 | end 30 | 31 | def rollback_prepared_transaction(transaction) 32 | cmd = "ROLLBACK" 33 | transaction.connection.create_command(cmd).execute_non_query 34 | DataMapper.logger.debug("#{self}: #{cmd}") 35 | end 36 | 37 | TYPES.merge!( 38 | :integer => 'INTEGER'.freeze, 39 | :string => 'TEXT'.freeze, 40 | :text => 'TEXT'.freeze, 41 | :class => 'TEXT'.freeze, 42 | :boolean => 'INTEGER'.freeze 43 | ) 44 | 45 | def rewrite_uri(uri, options) 46 | new_uri = uri.dup 47 | new_uri.path = options[:path] || uri.path 48 | 49 | new_uri 50 | end 51 | 52 | protected 53 | 54 | def normalize_uri(uri_or_options) 55 | uri = super 56 | uri.path = File.join(Dir.pwd, File.dirname(uri.path), File.basename(uri.path)) unless File.exists?(uri.path) 57 | uri 58 | end 59 | end # class Sqlite3Adapter 60 | 61 | end # module Adapters 62 | end # module DataMapper 63 | -------------------------------------------------------------------------------- /lib/data_mapper/associations.rb: -------------------------------------------------------------------------------- 1 | dir = Pathname(__FILE__).dirname.expand_path / 'associations' 2 | 3 | require dir / 'relationship' 4 | require dir / 'many_to_many' 5 | require dir / 'many_to_one' 6 | require dir / 'one_to_many' 7 | require dir / 'one_to_one' 8 | 9 | module DataMapper 10 | module Associations 11 | def self.extended(base) 12 | base.extend ManyToOne 13 | base.extend OneToMany 14 | base.extend ManyToMany 15 | base.extend OneToOne 16 | end 17 | 18 | def relationships(repository_name) 19 | (@relationships ||= Hash.new { |h, k| h[k] = {} })[repository_name] 20 | end 21 | 22 | def n 23 | 1.0/0 24 | end 25 | 26 | # 27 | # A shorthand, clear syntax for defining one-to-one, one-to-many and many-to-many resource relationships. 28 | # 29 | # ==== Usage Examples... 30 | # * has 1, :friend # one_to_one, :friend 31 | # * has n, :friends # one_to_many :friends 32 | # * has 1..3, :friends # one_to_many :friends, :min => 1, :max => 3 33 | # * has 3, :friends # one_to_many :friends, :min => 3, :max => 3 34 | # * has 1, :friend, :class_name=>'User' # one_to_one :friend, :class_name => 'User' 35 | # * has 3, :friends, :through=>:friendships # one_to_many :friends, :through => :friendships 36 | # 37 | # ==== Parameters 38 | # cardinality:: Defines the association type & constraints 39 | # name:: The name that the association will be referenced by 40 | # opts:: An options hash (see below) 41 | # 42 | # ==== Options (opts) 43 | # :through:: A association that this join should go through to form a many-to-many association 44 | # :class_name:: The name of the class to associate with, if ommitted then the association name is assumed to match the class name 45 | # 46 | # ==== Returns 47 | # DataMapper::Association::Relationship:: The relationship that was created to reflect either a one-to-one, one-to-many or many-to-many relationship 48 | # 49 | # ==== Raises 50 | # ArgumentError:: if the cardinality was not understood - should be Fixnum, Bignum, Infinity(n) or Range 51 | # 52 | # @public 53 | def has(cardinality, name, options = {}) 54 | options = options.merge(extract_min_max(cardinality)) 55 | relationship = nil 56 | if options[:max] == 1 57 | relationship = one_to_one(name, options) 58 | else 59 | relationship = one_to_many(name, options) 60 | end 61 | # Please leave this in - I will release contextual serialization soon which requires this -- guyvdb 62 | # TODO convert this to a hook in the plugin once hooks work on class methods 63 | self.init_has_relationship_for_serialization(relationship) if self.respond_to?(:init_has_relationship_for_serialization) 64 | end 65 | 66 | # 67 | # A shorthand, clear syntax for defining many-to-one resource relationships. 68 | # 69 | # ==== Usage Examples... 70 | # * belongs_to :user # many_to_one, :friend 71 | # * belongs_to :friend, :classname => 'User' # many_to_one :friends 72 | # 73 | # ==== Parameters 74 | # name:: The name that the association will be referenced by 75 | # opts:: An options hash (see below) 76 | # 77 | # ==== Options (opts) 78 | # (See has() for options) 79 | # 80 | # ==== Returns 81 | # DataMapper::Association::ManyToOne:: The association created should not be accessed directly 82 | # 83 | # @public 84 | def belongs_to(name, options={}) 85 | relationship = many_to_one(name, options) 86 | # Please leave this in - I will release contextual serialization soon which requires this -- guyvdb 87 | # TODO convert this to a hook in the plugin once hooks work on class methods 88 | self.init_belongs_relationship_for_serialization(relationship) if self.respond_to?(:init_belongs_relationship_for_serialization) 89 | end 90 | 91 | 92 | private 93 | 94 | # A support method form converting Fixnum, Range or Infinity values into a {:min=>x, :max=>y} hash. 95 | # 96 | # @private 97 | def extract_min_max(constraints) 98 | case constraints 99 | when Range 100 | raise ArgumentError, "Constraint min (#{constraints.first}) cannot be larger than the max (#{constraints.last})" if constraints.first > constraints.last 101 | { :min => constraints.first, :max => constraints.last } 102 | when Fixnum, Bignum 103 | { :min => constraints, :max => constraints } 104 | when n 105 | {} 106 | else 107 | raise ArgumentError, "Constraint #{constraints.inspect} (#{constraints.class}) not handled must be one of Range, Fixnum, Bignum, Infinity(n)" 108 | end 109 | end 110 | end # module Associations 111 | end # module DataMapper 112 | -------------------------------------------------------------------------------- /lib/data_mapper/associations/many_to_many.rb: -------------------------------------------------------------------------------- 1 | module DataMapper 2 | module Associations 3 | module ManyToMany 4 | private 5 | def many_to_many(name, options = {}) 6 | raise ArgumentError, "+name+ should be a Symbol, but was #{name.class}", caller unless Symbol === name 7 | raise ArgumentError, "+options+ should be a Hash, but was #{options.class}", caller unless Hash === options 8 | 9 | # TOOD: raise an exception if unknown options are passed in 10 | 11 | child_model_name = DataMapper::Inflection.demodulize(self.name) 12 | parent_model_name = options[:class_name] || DataMapper::Inflection.classify(name) 13 | 14 | relationships(repository.name)[name] = Relationship.new( 15 | name, 16 | options, 17 | repository.name, 18 | child_model_name, 19 | nil, 20 | parent_model_name, 21 | nil 22 | ) 23 | relationships(repository.name)[name] 24 | end 25 | 26 | class Instance 27 | def initialize() end 28 | 29 | def save 30 | raise NotImplementedError 31 | end 32 | 33 | end # class Instance 34 | end # module ManyToMany 35 | end # module Associations 36 | end # module DataMapper 37 | -------------------------------------------------------------------------------- /lib/data_mapper/associations/many_to_one.rb: -------------------------------------------------------------------------------- 1 | module DataMapper 2 | module Associations 3 | module ManyToOne 4 | def many_to_one(name, options = {}) 5 | raise ArgumentError, "+name+ should be a Symbol, but was #{name.class}", caller unless Symbol === name 6 | raise ArgumentError, "+options+ should be a Hash, but was #{options.class}", caller unless Hash === options 7 | 8 | # TOOD: raise an exception if unknown options are passed in 9 | 10 | child_model_name = DataMapper::Inflection.demodulize(self.name) 11 | parent_model_name = options[:class_name] || DataMapper::Inflection.classify(name) 12 | 13 | relationships(repository.name)[name] = Relationship.new( 14 | name, 15 | options, 16 | repository.name, 17 | child_model_name, 18 | nil, 19 | parent_model_name, 20 | nil 21 | ) 22 | 23 | class_eval <<-EOS, __FILE__, __LINE__ 24 | def #{name} 25 | #{name}_association.parent 26 | end 27 | 28 | def #{name}=(parent_resource) 29 | #{name}_association.parent = parent_resource 30 | end 31 | 32 | private 33 | 34 | def #{name}_association 35 | @#{name}_association ||= begin 36 | relationship = self.class.relationships(repository.name)[:#{name}] 37 | 38 | association = relationship.with_child(self, Instance) do |repository, child_key, parent_key, parent_model, child_resource| 39 | repository.all(parent_model, parent_key.to_query(child_key.get(child_resource))).first 40 | end 41 | 42 | child_associations << association 43 | 44 | association 45 | end 46 | end 47 | EOS 48 | relationships(repository.name)[name] 49 | end 50 | 51 | class Instance 52 | def parent 53 | @parent_resource ||= @parent_loader.call 54 | end 55 | 56 | def parent=(parent_resource) 57 | @parent_resource = parent_resource 58 | 59 | @relationship.attach_parent(@child_resource, @parent_resource) if @parent_resource.nil? || !@parent_resource.new_record? 60 | end 61 | 62 | def loaded? 63 | !defined?(@parent_resource) 64 | end 65 | 66 | def save 67 | if parent.new_record? 68 | repository(@relationship.repository_name).save(parent) 69 | @relationship.attach_parent(@child_resource, parent) 70 | end 71 | end 72 | 73 | private 74 | 75 | def initialize(relationship, child_resource, &parent_loader) 76 | # raise ArgumentError, "+relationship+ should be a DataMapper::Association::Relationship, but was #{relationship.class}", caller unless Relationship === relationship 77 | # raise ArgumentError, "+child_resource+ should be a DataMapper::Resource, but was #{child_resource.class}", caller unless Resource === child_resource 78 | 79 | @relationship = relationship 80 | @child_resource = child_resource 81 | @parent_loader = parent_loader 82 | end 83 | end # class Instance 84 | end # module ManyToOne 85 | end # module Associations 86 | end # module DataMapper 87 | -------------------------------------------------------------------------------- /lib/data_mapper/associations/one_to_many.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | module DataMapper 4 | module Associations 5 | module OneToMany 6 | private 7 | 8 | def one_to_many(name, options = {}) 9 | raise ArgumentError, "+name+ should be a Symbol, but was #{name.class}", caller unless Symbol === name 10 | raise ArgumentError, "+options+ should be a Hash, but was #{options.class}", caller unless Hash === options 11 | 12 | # TOOD: raise an exception if unknown options are passed in 13 | 14 | child_model_name = options[:class_name] || DataMapper::Inflection.classify(name) 15 | 16 | relationships(repository.name)[name] = Relationship.new( 17 | DataMapper::Inflection.underscore(self.name).to_sym, 18 | options, 19 | repository.name, 20 | child_model_name, 21 | nil, 22 | self.name, 23 | nil 24 | ) 25 | 26 | class_eval <<-EOS, __FILE__, __LINE__ 27 | def #{name} 28 | #{name}_association 29 | end 30 | 31 | private 32 | 33 | def #{name}_association 34 | @#{name}_association ||= begin 35 | relationship = self.class.relationships(repository.name)[:#{name}] 36 | 37 | association = Proxy.new(relationship, self) do |repository, relationship| 38 | repository.all(*relationship.to_child_query(self)) 39 | end 40 | 41 | parent_associations << association 42 | 43 | association 44 | end 45 | end 46 | EOS 47 | 48 | relationships(repository.name)[name] 49 | end 50 | 51 | class Proxy 52 | extend Forwardable 53 | include Enumerable 54 | 55 | def_instance_delegators :entries, :[], :size, :length, :first, :last 56 | 57 | def loaded? 58 | !defined?(@children_resources) 59 | end 60 | 61 | def clear 62 | each { |child_resource| delete(child_resource) } 63 | end 64 | 65 | def each(&block) 66 | children.each(&block) 67 | self 68 | end 69 | 70 | def children 71 | @children_resources ||= @children_loader.call(repository(@relationship.repository_name), @relationship) 72 | end 73 | 74 | def save 75 | @dirty_children.each do |child_resource| 76 | save_child(child_resource) 77 | end 78 | end 79 | 80 | def push(*child_resources) 81 | child_resources.each do |child_resource| 82 | if @parent_resource.new_record? 83 | @dirty_children << child_resource 84 | else 85 | save_child(child_resource) 86 | end 87 | 88 | children << child_resource 89 | end 90 | 91 | self 92 | end 93 | 94 | alias << push 95 | 96 | def delete(child_resource) 97 | deleted_resource = children.delete(child_resource) 98 | begin 99 | @relationship.attach_parent(deleted_resource, nil) 100 | repository(@relationship.repository_name).save(deleted_resource) 101 | rescue 102 | children << child_resource 103 | raise 104 | end 105 | end 106 | 107 | private 108 | 109 | def initialize(relationship, parent_resource, &children_loader) 110 | # raise ArgumentError, "+relationship+ should be a DataMapper::Association::Relationship, but was #{relationship.class}", caller unless Relationship === relationship 111 | # raise ArgumentError, "+parent_resource+ should be a DataMapper::Resource, but was #{parent_resource.class}", caller unless Resource === parent_resource 112 | 113 | @relationship = relationship 114 | @parent_resource = parent_resource 115 | @children_loader = children_loader 116 | @dirty_children = [] 117 | end 118 | 119 | def save_child(child_resource) 120 | @relationship.attach_parent(child_resource, @parent_resource) 121 | repository(@relationship.repository_name).save(child_resource) 122 | end 123 | end # class Proxy 124 | end # module OneToMany 125 | end # module Associations 126 | end # module DataMapper 127 | -------------------------------------------------------------------------------- /lib/data_mapper/associations/one_to_one.rb: -------------------------------------------------------------------------------- 1 | module DataMapper 2 | module Associations 3 | module OneToOne 4 | private 5 | def one_to_one(name, options = {}) 6 | raise ArgumentError, "+name+ should be a Symbol, but was #{name.class}", caller unless Symbol === name 7 | raise ArgumentError, "+options+ should be a Hash, but was #{options.class}", caller unless Hash === options 8 | 9 | # TOOD: raise an exception if unknown options are passed in 10 | 11 | child_model_name = options[:class_name] || DataMapper::Inflection.classify(name) 12 | parent_model_name = DataMapper::Inflection.demodulize(self.name) 13 | 14 | relationships(repository.name)[name] = Relationship.new( 15 | DataMapper::Inflection.underscore(parent_model_name).to_sym, 16 | options, 17 | repository.name, 18 | child_model_name, 19 | nil, 20 | parent_model_name, 21 | nil 22 | ) 23 | 24 | class_eval <<-EOS, __FILE__, __LINE__ 25 | def #{name} 26 | #{name}_association.first 27 | end 28 | 29 | def #{name}=(child_resource) 30 | #{name}_association.clear 31 | #{name}_association << child_resource unless child_resource.nil? 32 | end 33 | 34 | private 35 | 36 | def #{name}_association 37 | @#{name}_association ||= begin 38 | relationship = self.class.relationships(repository.name)[:#{name}] 39 | 40 | association = Associations::OneToMany::Proxy.new(relationship, self) do |repository, relationship| 41 | repository.all(*relationship.to_child_query(self)) 42 | end 43 | 44 | parent_associations << association 45 | 46 | association 47 | end 48 | end 49 | EOS 50 | relationships(repository.name)[name] 51 | end 52 | 53 | end # module HasOne 54 | end # module Associations 55 | end # module DataMapper 56 | -------------------------------------------------------------------------------- /lib/data_mapper/associations/relationship.rb: -------------------------------------------------------------------------------- 1 | module DataMapper 2 | module Associations 3 | class Relationship 4 | 5 | attr_reader :name, :repository_name, :options 6 | 7 | def child_key 8 | @child_key ||= begin 9 | model_properties = child_model.properties(@repository_name) 10 | 11 | child_key = parent_key.zip(@child_properties || []).map do |parent_property,property_name| 12 | # TODO: use something similar to DM::NamingConventions to determine the property name 13 | property_name ||= "#{@name}_#{parent_property.name}".to_sym 14 | model_properties[property_name] || child_model.property(property_name, parent_property.type) 15 | end 16 | 17 | PropertySet.new(child_key) 18 | end 19 | end 20 | 21 | def parent_key 22 | @parent_key ||= begin 23 | model_properties = parent_model.properties(@repository_name) 24 | 25 | parent_key = if @parent_properties 26 | model_properties.slice(*@parent_properties) 27 | else 28 | model_properties.key 29 | end 30 | 31 | PropertySet.new(parent_key) 32 | end 33 | end 34 | 35 | 36 | def to_child_query(parent) 37 | [child_model, child_key.to_query(parent_key.get(parent))] 38 | end 39 | 40 | def with_child(child_resource, association, &loader) 41 | association.new(self, child_resource) do 42 | yield repository(@repository_name), child_key, parent_key, parent_model, child_resource 43 | end 44 | end 45 | 46 | def with_parent(parent_resource, association, &loader) 47 | association.new(self, parent_resource) do 48 | yield repository(@repository_name), child_key, parent_key, child_model, parent_resource 49 | end 50 | end 51 | 52 | def attach_parent(child, parent) 53 | child_key.set(child, parent && parent_key.get(parent)) 54 | end 55 | 56 | def parent_model 57 | @parent_model_name.to_class 58 | end 59 | 60 | def child_model 61 | @child_model_name.to_class 62 | end 63 | 64 | private 65 | 66 | # +child_model_name and child_properties refers to the FK, parent_model_name 67 | # and parent_properties refer to the PK. For more information: 68 | # http://edocs.bea.com/kodo/docs41/full/html/jdo_overview_mapping_join.html 69 | # I wash my hands of it! 70 | 71 | # FIXME: should we replace child_* and parent_* arguments with two 72 | # Arrays of Property objects? This would allow syntax like: 73 | # 74 | # belongs_to = DataMapper::Associations::Relationship.new( 75 | # :manufacturer, 76 | # :relationship_spec, 77 | # Vehicle.properties.slice(:manufacturer_id) 78 | # Manufacturer.properties.slice(:id) 79 | # ) 80 | def initialize(name,options, repository_name, child_model_name, child_properties, parent_model_name, parent_properties, &loader) 81 | raise ArgumentError, "+name+ should be a Symbol, but was #{name.class}", caller unless Symbol === name 82 | raise ArgumentError, "+repository_name+ must be a Symbol, but was #{repository_name.class}", caller unless Symbol === repository_name 83 | raise ArgumentError, "+child_model_name+ must be a String, but was #{child_model_name.class}", caller unless String === child_model_name 84 | raise ArgumentError, "+child_properties+ must be an Array or nil, but was #{child_properties.class}", caller unless Array === child_properties || child_properties.nil? 85 | raise ArgumentError, "+parent_model_name+ must be a String, but was #{parent_model_name.class}", caller unless String === parent_model_name 86 | raise ArgumentError, "+parent_properties+ must be an Array or nil, but was #{parent_properties.class}", caller unless Array === parent_properties || parent_properties.nil? 87 | 88 | @name = name 89 | @options = options 90 | @repository_name = repository_name 91 | @child_model_name = child_model_name 92 | @child_properties = child_properties # may be nil 93 | @parent_model_name = parent_model_name 94 | @parent_properties = parent_properties # may be nil 95 | @loader = loader 96 | end 97 | end # class Relationship 98 | end # module Associations 99 | end # module DataMapper 100 | -------------------------------------------------------------------------------- /lib/data_mapper/hook.rb: -------------------------------------------------------------------------------- 1 | module DataMapper 2 | module Hook 3 | def self.included(base) 4 | base.extend(ClassMethods) 5 | end 6 | 7 | module ClassMethods 8 | # 9 | # Inject code that executes before the target_method. 10 | # 11 | # ==== Parameters 12 | # target_method:: The name of the class method to inject before 13 | # method_sym:: The name of the method to run before the target_method 14 | # block:: The code to run before the target_method 15 | # 16 | # Either method_sym or block is required. 17 | # 18 | # - 19 | # @public 20 | def before_class_method(target_method, method_sym = nil, &block) 21 | install_hook :before_class_method, target_method, method_sym, :class, &block 22 | end 23 | 24 | # 25 | # Inject code that executes after the target_method. 26 | # 27 | # ==== Parameters 28 | # target_method:: The name of the class method to inject after 29 | # method_sym:: The name of the method to run after the target_method 30 | # block:: The code to run after the target_method 31 | # 32 | # Either method_sym or block is required. 33 | # 34 | # - 35 | # @public 36 | def after_class_method(target_method, method_sym = nil, &block) 37 | install_hook :after_class_method, target_method, method_sym, :class, &block 38 | end 39 | 40 | # 41 | # Inject code that executes before the target_method. 42 | # 43 | # ==== Parameters 44 | # target_method:: The name of the instance method to inject before 45 | # method_sym:: The name of the method to run before the target_method 46 | # block:: The code to run before the target_method 47 | # 48 | # Either method_sym or block is required. 49 | # 50 | # - 51 | # @public 52 | def before(target_method, method_sym = nil, &block) 53 | install_hook :before, target_method, method_sym, :instance, &block 54 | end 55 | 56 | # 57 | # Inject code that executes after the target_method. 58 | # 59 | # ==== Parameters 60 | # target_method:: The name of the instance method to inject after 61 | # method_sym:: The name of the method to run after the target_method 62 | # block:: The code to run after the target_method 63 | # 64 | # Either method_sym or block is required. 65 | # 66 | # - 67 | # @public 68 | def after(target_method, method_sym = nil, &block) 69 | install_hook :after, target_method, method_sym, :instance, &block 70 | end 71 | 72 | def define_instance_or_class_method(new_meth_name, block, scope) 73 | if scope == :class 74 | class << self 75 | self 76 | end.instance_eval do 77 | define_method new_meth_name, block 78 | end 79 | elsif scope == :instance 80 | define_method new_meth_name, block 81 | else 82 | raise ArgumentError.new("You need to pass :class or :instance as scope") 83 | end 84 | end 85 | 86 | def hooks_with_scope(scope) 87 | if scope == :class 88 | class_method_hooks 89 | elsif scope == :instance 90 | hooks 91 | else 92 | raise ArgumentError.new("You need to pass :class or :instance as scope") 93 | end 94 | end 95 | 96 | def install_hook(type, name, method_sym, scope, &block) 97 | raise ArgumentError.new("You need to pass 2 arguments to \"#{type}\".") if ! block_given? and method_sym.nil? 98 | raise ArgumentError.new("target_method should be a symbol") unless name.is_a?(Symbol) 99 | raise ArgumentError.new("method_sym should be a symbol") if method_sym && ! method_sym.is_a?(Symbol) 100 | raise ArgumentError.new("You need to pass :class or :instance as scope") unless [:class, :instance].include?(scope) 101 | 102 | (hooks_with_scope(scope)[name][type] ||= []) << if block 103 | new_meth_name = "__hooks_#{scope}_#{type}_#{quote_method(name)}_#{hooks_with_scope(scope)[name][type].length}".to_sym 104 | define_instance_or_class_method(new_meth_name, block, scope) 105 | new_meth_name 106 | else 107 | method_sym 108 | end 109 | 110 | class_eval define_advised_method(name, scope), __FILE__, __LINE__ 111 | end 112 | 113 | def method_with_scope(name, scope) 114 | if scope == :class 115 | method(name) 116 | elsif scope == :instance 117 | instance_method(name) 118 | else 119 | raise ArgumentError.new("You need to pass :class or :instance as scope") 120 | end 121 | end 122 | 123 | # FIXME Return the method value 124 | def define_advised_method(name, scope) 125 | args = args_for(hooks_with_scope(scope)[name][:old_method] ||= method_with_scope(name, scope)) 126 | 127 | prefix = "" 128 | types = [:before, :after] 129 | if scope == :class 130 | prefix = "self." 131 | types = [:before_class_method, :after_class_method] 132 | elsif scope != :instance 133 | raise ArgumentError.new("You need to pass :class or :instance as scope") 134 | end 135 | 136 | <<-EOD 137 | def #{prefix}#{name}(#{args}) 138 | #{inline_hooks(name, scope, types.first, args)} 139 | retval = #{inline_call(name, scope, args)} 140 | #{inline_hooks(name, scope, types.last, args)} 141 | retval 142 | end 143 | EOD 144 | end 145 | 146 | def inline_call(name, scope, args) 147 | if scope == :class 148 | if (class << superclass; self; end.method_defined?(name)) 149 | " super(#{args})\n" 150 | else 151 | <<-EOF 152 | (@__hooks_#{scope}_#{quote_method(name)}_old_method || @__hooks_#{scope}_#{quote_method(name)}_old_method = 153 | self.class_method_hooks[:#{name}][:old_method]).call(#{args}) 154 | EOF 155 | end 156 | elsif scope == :instance 157 | if superclass.method_defined?(name) 158 | " super(#{args})\n" 159 | else 160 | <<-EOF 161 | (@__hooks_#{scope}_#{quote_method(name)}_old_method || @__hooks_#{scope}_#{quote_method(name)}_old_method = 162 | self.class.hooks[:#{name}][:old_method].bind(self)).call(#{args}) 163 | EOF 164 | end 165 | else 166 | raise ArgumentError.new("You need to pass :class or :instance as scope") 167 | end 168 | end 169 | 170 | def inline_hooks(name, scope, type, args) 171 | return '' unless hooks_with_scope(scope)[name][type] 172 | 173 | method_def = "" 174 | hooks_with_scope(scope)[name][type].each_with_index do |e, i| 175 | case e 176 | when Symbol 177 | method_def << " #{e}(#{args})\n" 178 | else 179 | # TODO: Test this. Testing order should be before, after and after, before 180 | method_def << "(@__hooks_#{scope}_#{quote_method(name)}_#{type}_#{i} || " 181 | method_def << " @__hooks_#{scope}_#{quote_method(name)}_#{type}_#{i} = self.class.hooks_with_scope(#{scope.inspect})[:#{name}][:#{type}][#{i}])" 182 | method_def << ".call #{args}\n" 183 | end 184 | end 185 | 186 | method_def 187 | end 188 | 189 | def args_for(method) 190 | if method.arity == 0 191 | "" 192 | elsif method.arity > 0 193 | "_" << (1 .. method.arity).to_a.join(", _") 194 | elsif (method.arity + 1) < 0 195 | "_" << (1 .. (method.arity).abs - 1).to_a.join(", _") << ", *args" 196 | else 197 | "*args" 198 | end 199 | end 200 | 201 | def hooks 202 | @hooks ||= Hash.new { |h, k| h[k] = {} } 203 | end 204 | 205 | def class_method_hooks 206 | @class_method_hooks ||= Hash.new { |h, k| h[k] = {} } 207 | end 208 | 209 | def quote_method(name) 210 | name.to_s.gsub(/\?$/, '_q_').gsub(/!$/, '_b_').gsub(/=$/, '_eq_') 211 | end 212 | end 213 | end # module Hook 214 | end # module DataMapper 215 | -------------------------------------------------------------------------------- /lib/data_mapper/identity_map.rb: -------------------------------------------------------------------------------- 1 | module DataMapper 2 | 3 | # Tracks objects to help ensure that each object gets loaded only once. 4 | # See: http://www.martinfowler.com/eaaCatalog/identityMap.html 5 | class IdentityMap 6 | # Get a resource from the IdentityMap 7 | def get(key) 8 | raise ArgumentError, "+key+ is not an Array, but was #{key.class}" unless Array === key 9 | 10 | @cache[key] 11 | end 12 | 13 | alias [] get 14 | 15 | # Add a resource to the IdentityMap 16 | def set(key, resource) 17 | raise ArgumentError, "+key+ is not an Array, but was #{key.class}" unless Array === key 18 | raise ArgumentError, "+resource+ should be a DataMapper::Resource, but was #{resource.class}" unless Resource === resource 19 | 20 | @second_level_cache.set(key, resource) if @second_level_cache 21 | @cache[key] = resource 22 | end 23 | 24 | alias []= set 25 | 26 | # Remove a resource from the IdentityMap 27 | def delete(key) 28 | raise ArgumentError, "+key+ is not an Array, but was #{key.class}" unless Array === key 29 | 30 | @second_level_cache.delete(key) if @second_level_cache 31 | @cache.delete(key) 32 | end 33 | 34 | private 35 | 36 | def initialize(second_level_cache = nil) 37 | @cache = if @second_level_cache = second_level_cache 38 | Hash.new { |h,key| h[key] = @second_level_cache.get(key) } 39 | else 40 | Hash.new 41 | end 42 | end 43 | end # class IdentityMap 44 | end # module DataMapper 45 | -------------------------------------------------------------------------------- /lib/data_mapper/loaded_set.rb: -------------------------------------------------------------------------------- 1 | require 'forwardable' 2 | 3 | module DataMapper 4 | class LoadedSet 5 | extend Forwardable 6 | include Enumerable 7 | 8 | def_instance_delegators :entries, :[], :size, :length, :first, :last 9 | 10 | attr_reader :repository 11 | 12 | def reload(options = {}) 13 | query = Query.new(@repository, @model, keys.merge(:fields => @key_properties)) 14 | query.update(options.merge(:reload => true)) 15 | @repository.adapter.read_set(@repository, query) 16 | end 17 | 18 | def load(values, reload = false) 19 | model = if @inheritance_property_index 20 | values.at(@inheritance_property_index) 21 | else 22 | @model 23 | end 24 | 25 | resource = nil 26 | 27 | if @key_property_indexes 28 | key_values = values.values_at(*@key_property_indexes) 29 | 30 | if resource = @repository.identity_map_get(model, key_values) 31 | self << resource 32 | return resource unless reload 33 | else 34 | resource = model.allocate 35 | self << resource 36 | @key_properties.zip(key_values).each do |property,key_value| 37 | resource.instance_variable_set(property.instance_variable_name, key_value) 38 | end 39 | resource.instance_variable_set(:@new_record, false) 40 | @repository.identity_map_set(resource) 41 | end 42 | else 43 | resource = model.allocate 44 | self << resource 45 | resource.instance_variable_set(:@new_record, false) 46 | resource.readonly! 47 | end 48 | 49 | @properties_with_indexes.each_pair do |property, i| 50 | resource.instance_variable_set(property.instance_variable_name, values.at(i)) 51 | end 52 | 53 | self 54 | end 55 | 56 | def add(resource) 57 | raise ArgumentError, "+resource+ should be a DataMapper::Resource, but was #{resource.class}" unless Resource === resource 58 | @resources << resource 59 | resource.loaded_set = self 60 | end 61 | 62 | alias << add 63 | 64 | def merge(*resources) 65 | resources.each { |resource| add(resource) } 66 | self 67 | end 68 | 69 | def delete(resource) 70 | raise ArgumentError, "+resource+ should be a DataMapper::Resource, but was #{resource.class}" unless Resource === resource 71 | @resources.delete(resource) 72 | end 73 | 74 | def entries 75 | @resources.dup 76 | end 77 | 78 | def each(&block) 79 | entries.each { |entry| yield entry } 80 | self 81 | end 82 | 83 | private 84 | 85 | # +properties_with_indexes+ is a Hash of Property and values Array index pairs. 86 | # { Property<:id> => 1, Property<:name> => 2, Property<:notes> => 3 } 87 | def initialize(repository, model, properties_with_indexes) 88 | raise ArgumentError, "+repository+ must be a DataMapper::Repository, but was #{repository.class}", caller unless Repository === repository 89 | raise ArgumentError, "+model+ is a #{model.class}, but is not a type of Resource", caller unless Resource > model 90 | 91 | @repository = repository 92 | @model = model 93 | @properties_with_indexes = properties_with_indexes 94 | @resources = [] 95 | 96 | if inheritance_property = @model.inheritance_property(@repository.name) 97 | @inheritance_property_index = @properties_with_indexes[inheritance_property] 98 | end 99 | 100 | if (@key_properties = @model.key(@repository.name)).all? { |key| @properties_with_indexes.include?(key) } 101 | @key_property_indexes = @properties_with_indexes.values_at(*@key_properties) 102 | end 103 | end 104 | 105 | def keys 106 | entry_keys = @resources.map { |resource| resource.key } 107 | 108 | keys = {} 109 | @key_properties.zip(entry_keys.transpose).each do |property,values| 110 | keys[property] = values 111 | end 112 | keys 113 | end 114 | end # class LoadedSet 115 | 116 | class LazyLoadedSet < LoadedSet 117 | def entries 118 | @loader[self] 119 | 120 | class << self 121 | def entries 122 | super 123 | end 124 | end 125 | 126 | super 127 | end 128 | 129 | private 130 | 131 | def initialize(*args, &block) 132 | raise "LazyLoadedSets require a materialization block. Use a LoadedSet instead." unless block_given? 133 | super 134 | @loader = block 135 | end 136 | end # class LazyLoadedSet 137 | end # module DataMapper 138 | -------------------------------------------------------------------------------- /lib/data_mapper/logger.rb: -------------------------------------------------------------------------------- 1 | require "time" # httpdate 2 | # ==== Public DataMapper Logger API 3 | # 4 | # Logger taken from Merb :) 5 | # 6 | # To replace an existing logger with a new one: 7 | # DataMapper::Logger.set_log(log{String, IO},level{Symbol, String}) 8 | # 9 | # Available logging levels are 10 | # DataMapper::Logger::{ Fatal, Error, Warn, Info, Debug } 11 | # 12 | # Logging via: 13 | # DataMapper.logger.fatal(message) 14 | # DataMapper.logger.error(message) 15 | # DataMapper.logger.warn(message) 16 | # DataMapper.logger.info(message) 17 | # DataMapper.logger.debug(message) 18 | # 19 | # Flush the buffer to 20 | # DataMapper.logger.flush 21 | # 22 | # Remove the current log object 23 | # DataMapper.logger.close 24 | # 25 | # ==== Private DataMapper Logger API 26 | # 27 | # To initialize the logger you create a new object, proxies to set_log. 28 | # DataMapper::Logger.new(log{String, IO},level{Symbol, String}) 29 | module DataMapper 30 | 31 | class << self #:nodoc: 32 | attr_accessor :logger 33 | end 34 | 35 | class Logger 36 | 37 | attr_accessor :aio 38 | attr_accessor :level 39 | attr_accessor :delimiter 40 | attr_reader :buffer 41 | attr_reader :log 42 | 43 | # Note: 44 | # Ruby (standard) logger levels: 45 | # fatal: an unhandleable error that results in a program crash 46 | # error: a handleable error condition 47 | # warn: a warning 48 | # info: generic (useful) information about system operation 49 | # debug: low-level information for developers 50 | # 51 | # DataMapper::Logger::LEVELS[:fatal, :error, :warn, :info, :debug] 52 | LEVELS = 53 | { 54 | :fatal => 7, 55 | :error => 6, 56 | :warn => 4, 57 | :info => 3, 58 | :debug => 0 59 | } 60 | 61 | private 62 | 63 | # The idea here is that instead of performing an 'if' conditional check 64 | # on each logging we do it once when the log object is setup 65 | def set_write_method 66 | @log.instance_eval do 67 | 68 | # Determine if asynchronous IO can be used 69 | def aio? 70 | @aio = !RUBY_PLATFORM.match(/java|mswin/) && 71 | !(@log == STDOUT) && 72 | @log.respond_to?(:write_nonblock) 73 | end 74 | 75 | # Define the write method based on if aio an be used 76 | undef write_method if defined? write_method 77 | if aio? 78 | alias :write_method :write_nonblock 79 | else 80 | alias :write_method :write 81 | end 82 | end 83 | end 84 | 85 | def initialize_log(log) 86 | close if @log # be sure that we don't leave open files laying around. 87 | log ||= "log/dm.log" 88 | if log.respond_to?(:write) 89 | @log = log 90 | else 91 | log = Pathname(log) 92 | log.dirname.mkpath 93 | @log = log.open('a') 94 | @log.sync = true 95 | @log.write("#{Time.now.httpdate} #{delimiter} info #{delimiter} Logfile created\n") 96 | end 97 | set_write_method 98 | end 99 | 100 | public 101 | 102 | # To initialize the logger you create a new object, proxies to set_log. 103 | # DataMapper::Logger.new(log{String, IO},level{Symbol, String}) 104 | # 105 | # ==== Parameters 106 | # log 107 | # Either an IO object or a name of a logfile. 108 | # log_level 109 | # The string message to be logged 110 | # delimiter 111 | # Delimiter to use between message sections 112 | def initialize(*args) 113 | set_log(*args) 114 | end 115 | 116 | # To replace an existing logger with a new one: 117 | # DataMapper::Logger.set_log(log{String, IO},level{Symbol, String}) 118 | # 119 | # ==== Parameters 120 | # log 121 | # Either an IO object or a name of a logfile. 122 | # log_level 123 | # A symbol representing the log level from {:fatal, :error, :warn, :info, :debug} 124 | # delimiter 125 | # Delimiter to use between message sections 126 | def set_log(log, log_level = nil, delimiter = " ~ ") 127 | if log_level && LEVELS[log_level.to_sym] 128 | @level = LEVELS[log_level.to_sym] 129 | else 130 | @level = LEVELS[:debug] 131 | end 132 | @buffer = [] 133 | @delimiter = delimiter 134 | 135 | initialize_log(log) 136 | 137 | DataMapper.logger = self 138 | end 139 | 140 | # Flush the entire buffer to the log object. 141 | # DataMapper.logger.flush 142 | # ==== Parameters 143 | # none 144 | def flush 145 | return unless @buffer.size > 0 146 | @log.write_method(@buffer.slice!(0..-1).to_s) 147 | end 148 | 149 | # Close and remove the current log object. 150 | # DataMapper.logger.close 151 | # ==== Parameters 152 | # none 153 | def close 154 | flush 155 | @log.close if @log.respond_to?(:close) 156 | @log = nil 157 | end 158 | 159 | # Appends a string and log level to logger's buffer. 160 | # Note that the string is discarded if the string's log level less than the logger's log level. 161 | # Note that if the logger is aio capable then the logger will use non-blocking asynchronous writes. 162 | # 163 | # ==== Parameters 164 | # level 165 | # The logging level as an integer 166 | # string 167 | # The string message to be logged 168 | def push(string) 169 | message = Time.now.httpdate 170 | message << delimiter 171 | message << string 172 | message << "\n" unless message[-1] == ?\n 173 | @buffer << message 174 | flush # Force a flush for now until we figure out where we want to use the buffering. 175 | end 176 | alias << push 177 | 178 | # Generate the following logging methods for DataMapper.logger as described in the api: 179 | # :fatal, :error, :warn, :info, :debug 180 | LEVELS.each_pair do |name, number| 181 | class_eval <<-LEVELMETHODS, __FILE__, __LINE__ 182 | # DOC 183 | def #{name}(message) 184 | self.<<(message) if #{name}? 185 | end 186 | 187 | # DOC 188 | def #{name}? 189 | #{number} >= level 190 | end 191 | LEVELMETHODS 192 | end 193 | 194 | end # class Logger 195 | 196 | end # module DataMapper 197 | -------------------------------------------------------------------------------- /lib/data_mapper/naming_conventions.rb: -------------------------------------------------------------------------------- 1 | module DataMapper 2 | 3 | # Use these modules to set naming conventions. 4 | # The default is UnderscoredAndPluralized. 5 | # You assign a naming convention like so: 6 | # 7 | # repository(:default).adapter.resource_naming_convention = NamingConventions::Underscored 8 | # 9 | # You can also easily assign a custom convention with a Proc: 10 | # 11 | # repository(:default).adapter.resource_naming_convention = lambda do |value| 12 | # 'tbl' + value.camelize(true) 13 | # end 14 | # 15 | # Or by simply defining your own module in NamingConventions that responds to ::call. 16 | # 17 | # NOTE: It's important to set the convention before accessing your models since the resource_names 18 | # are cached after first accessed. DataMapper.setup(name, uri) returns the Adapter for convenience, 19 | # so you can use code like this: 20 | # 21 | # adapter = DataMapper.setup(:default, "mock://localhost/mock") 22 | # adapter.resource_naming_convention = DataMapper::NamingConventions::Underscored 23 | module NamingConventions 24 | 25 | module UnderscoredAndPluralized 26 | def self.call(value) 27 | DataMapper::Inflection.pluralize(DataMapper::Inflection.underscore(value)).gsub('/','_') 28 | end 29 | end # module UnderscoredAndPluralized 30 | 31 | module UnderscoredAndPluralizedWithoutModule 32 | def self.call(value) 33 | DataMapper::Inflection.pluralize(DataMapper::Inflection.underscore(DataMapper::Inflection.demodulize(value))) 34 | end 35 | end # module UnderscoredAndPluralizedWithoutModule 36 | 37 | module Underscored 38 | def self.call(value) 39 | DataMapper::Inflection.underscore(value) 40 | end 41 | end # module Underscored 42 | 43 | module Yaml 44 | def self.call(value) 45 | DataMapper::Inflection.pluralize(DataMapper::Inflection.underscore(value)) + ".yaml" 46 | end 47 | end # module Yaml 48 | 49 | end # module NamingConventions 50 | end # module DataMapper 51 | -------------------------------------------------------------------------------- /lib/data_mapper/property_set.rb: -------------------------------------------------------------------------------- 1 | module DataMapper 2 | class PropertySet 3 | include Enumerable 4 | 5 | def [](name) 6 | @property_for[name] 7 | end 8 | 9 | alias has_property? [] 10 | 11 | def slice(*names) 12 | @property_for.values_at(*names) 13 | end 14 | 15 | def add(*properties) 16 | @entries.push(*properties) 17 | self 18 | end 19 | 20 | alias << add 21 | 22 | def length 23 | @entries.length 24 | end 25 | 26 | def empty? 27 | @entries.empty? 28 | end 29 | 30 | def each 31 | @entries.each { |property| yield property } 32 | self 33 | end 34 | 35 | def defaults 36 | @defaults ||= reject { |property| property.lazy? } 37 | end 38 | 39 | def key 40 | @key ||= select { |property| property.key? } 41 | end 42 | 43 | def inheritance_property 44 | @inheritance_property ||= detect { |property| property.type == Class } 45 | end 46 | 47 | def get(resource) 48 | map { |property| property.get(resource) } 49 | end 50 | 51 | def set(resource, values) 52 | raise ArgumentError, "+resource+ should be a DataMapper::Resource, but was #{resource.class}" unless Resource === resource 53 | if Array === values 54 | raise ArgumentError, "+values+ must have a length of #{length}, but has #{values.length}", caller if values.length != length 55 | elsif !values.nil? 56 | raise ArgumentError, "+values+ must be nil or an Array, but was a #{values.class}", caller 57 | end 58 | 59 | each_with_index { |property,i| property.set(resource, values.nil? ? nil : values[i]) } 60 | end 61 | 62 | def property_contexts(name) 63 | contexts = [] 64 | lazy_contexts.each do |context,property_names| 65 | contexts << context if property_names.include?(name) 66 | end 67 | contexts 68 | end 69 | 70 | def lazy_context(name) 71 | lazy_contexts[name] 72 | end 73 | 74 | def lazy_load_context(names) 75 | if Array === names 76 | raise ArgumentError, "+names+ cannot be an empty Array", caller if names.empty? 77 | elsif !(Symbol === names) 78 | raise ArgumentError, "+names+ must be a Symbol or an Array of Symbols, but was a #{names.class}", caller 79 | end 80 | 81 | result = [] 82 | 83 | Array(names).each do |name| 84 | contexts = property_contexts(name) 85 | if contexts.empty? 86 | result << name # not lazy 87 | else 88 | result |= lazy_contexts.values_at(*contexts).flatten.uniq 89 | end 90 | end 91 | result 92 | end 93 | 94 | def to_query(values) 95 | Hash[ *zip(values).flatten ] 96 | end 97 | 98 | def inspect 99 | '#' 100 | end 101 | 102 | private 103 | 104 | def initialize(properties = []) 105 | raise ArgumentError, "+properties+ should be an Array, but was #{properties.class}", caller unless Array === properties 106 | 107 | @entries = properties 108 | @property_for = hash_for_property_for 109 | end 110 | 111 | def initialize_copy(orig) 112 | @entries = orig.entries.dup 113 | @property_for = hash_for_property_for 114 | end 115 | 116 | def hash_for_property_for 117 | Hash.new do |h,k| 118 | raise "Key must be a Symbol or String, but was #{k.class}" unless [String, Symbol].include?(k.class) 119 | 120 | ksym = k.to_sym 121 | if property = detect { |property| property.name == ksym } 122 | h[ksym] = h[k.to_s] = property 123 | end 124 | end 125 | end 126 | 127 | def lazy_contexts 128 | @lazy_contexts ||= Hash.new { |h,context| h[context] = [] } 129 | end 130 | end # class PropertySet 131 | end # module DataMapper 132 | -------------------------------------------------------------------------------- /lib/data_mapper/repository.rb: -------------------------------------------------------------------------------- 1 | module DataMapper 2 | class Repository 3 | @adapters = {} 4 | 5 | def self.adapters 6 | @adapters 7 | end 8 | 9 | def self.context 10 | Thread.current[:dm_repository_contexts] ||= [] 11 | end 12 | 13 | def self.default_name 14 | :default 15 | end 16 | 17 | attr_reader :name, :adapter 18 | 19 | def identity_map_get(model, key) 20 | @identity_maps[model][key] 21 | end 22 | 23 | def identity_map_set(resource) 24 | @identity_maps[resource.class][resource.key] = resource 25 | end 26 | 27 | # TODO: this should use current_scope too 28 | def get(model, key) 29 | @identity_maps[model][key] || @adapter.read(self, model, key) 30 | end 31 | 32 | def first(model, options) 33 | query = if current_scope = model.send(:current_scope) 34 | current_scope.merge(options.merge(:limit => 1)) 35 | else 36 | Query.new(self, model, options.merge(:limit => 1)) 37 | end 38 | @adapter.read_set(self, query).first 39 | end 40 | 41 | def all(model, options) 42 | query = if current_scope = model.send(:current_scope) 43 | current_scope.merge(options) 44 | else 45 | Query.new(self, model, options) 46 | end 47 | @adapter.read_set(self, query).entries 48 | end 49 | 50 | def save(resource) 51 | resource.child_associations.each { |a| a.save } 52 | 53 | success = if resource.new_record? 54 | if @adapter.create(self, resource) 55 | identity_map_set(resource) 56 | resource.instance_variable_set(:@new_record, false) 57 | resource.dirty_attributes.clear 58 | true 59 | else 60 | false 61 | end 62 | else 63 | if @adapter.update(self, resource) 64 | resource.dirty_attributes.clear 65 | true 66 | else 67 | false 68 | end 69 | end 70 | 71 | resource.parent_associations.each { |a| a.save } 72 | success 73 | end 74 | 75 | def destroy(resource) 76 | if @adapter.delete(self, resource) 77 | @identity_maps[resource.class].delete(resource.key) 78 | resource.instance_variable_set(:@new_record, true) 79 | resource.dirty_attributes.clear 80 | resource.class.properties(name).each do |property| 81 | resource.dirty_attributes << property if resource.attribute_loaded?(property.name) 82 | end 83 | true 84 | else 85 | false 86 | end 87 | end 88 | 89 | # 90 | # Produce a new Transaction for this Repository. 91 | # 92 | # ==== Returns 93 | # DataMapper::Adapters::Transaction:: A new Transaction (in state :none) that can be used to execute 94 | # code #with_transaction. 95 | # 96 | def transaction 97 | DataMapper::Adapters::Transaction.new(self) 98 | end 99 | 100 | def to_s 101 | "#" 102 | end 103 | 104 | private 105 | 106 | def initialize(name) 107 | raise ArgumentError, "+name+ should be a Symbol, but was #{name.class}", caller unless Symbol === name 108 | 109 | @name = name 110 | @adapter = self.class.adapters[name] 111 | @identity_maps = Hash.new { |h,model| h[model] = IdentityMap.new } 112 | end 113 | 114 | end # class Repository 115 | end # module DataMapper 116 | -------------------------------------------------------------------------------- /lib/data_mapper/scope.rb: -------------------------------------------------------------------------------- 1 | module DataMapper 2 | module Scope 3 | def self.included(base) 4 | base.extend(ClassMethods) 5 | end 6 | 7 | module ClassMethods 8 | protected 9 | 10 | def with_scope(query, &block) 11 | # merge the current scope with the passed in query 12 | with_exclusive_scope(current_scope ? current_scope.merge(query) : query, &block) 13 | end 14 | 15 | def with_exclusive_scope(query, &block) 16 | query = DataMapper::Query.new(repository, self, query) if Hash === query 17 | 18 | scope_stack << query 19 | 20 | begin 21 | yield 22 | ensure 23 | scope_stack.pop 24 | end 25 | end 26 | 27 | private 28 | 29 | def scope_stack 30 | scope_stack_for = Thread.current[:dm_scope_stack] ||= Hash.new { |h,k| h[k] = [] } 31 | scope_stack_for[self] 32 | end 33 | 34 | def current_scope 35 | scope_stack.last 36 | end 37 | end # module ClassMethods 38 | end # module Scope 39 | end # module DataMapper 40 | -------------------------------------------------------------------------------- /lib/data_mapper/support.rb: -------------------------------------------------------------------------------- 1 | dir = Pathname(__FILE__).dirname.expand_path / 'support' 2 | 3 | require dir / 'blank' 4 | require dir / 'errors' 5 | require dir / 'inflection' 6 | require dir / 'kernel' 7 | require dir / 'object' 8 | require dir / 'pathname' 9 | require dir / 'string' 10 | require dir / 'struct' 11 | require dir / 'symbol' 12 | -------------------------------------------------------------------------------- /lib/data_mapper/support/blank.rb: -------------------------------------------------------------------------------- 1 | # blank? methods for several different class types 2 | class Object 3 | # Returns true if the object is nil or empty (if applicable) 4 | def blank? 5 | nil? || (respond_to?(:empty?) && empty?) 6 | end 7 | end # class Object 8 | 9 | class Numeric 10 | # Numerics can't be blank 11 | def blank? 12 | false 13 | end 14 | end # class Numeric 15 | 16 | class NilClass 17 | # Nils are always blank 18 | def blank? 19 | true 20 | end 21 | end # class NilClass 22 | 23 | class TrueClass 24 | # True is not blank. 25 | def blank? 26 | false 27 | end 28 | end # class TrueClass 29 | 30 | class FalseClass 31 | # False is always blank. 32 | def blank? 33 | true 34 | end 35 | end # class FalseClass 36 | 37 | class String 38 | # Strips out whitespace then tests if the string is empty. 39 | def blank? 40 | strip.empty? 41 | end 42 | end # class String 43 | -------------------------------------------------------------------------------- /lib/data_mapper/support/errors.rb: -------------------------------------------------------------------------------- 1 | #Some useful errors types 2 | module DataMapper 3 | class ValidationError < StandardError; end 4 | 5 | class ObjectNotFoundError < StandardError; end 6 | 7 | class MaterializationError < StandardError; end 8 | 9 | class RepositoryNotSetupError < StandardError; end 10 | 11 | class IncompleteResourceError < StandardError; end 12 | end # module DataMapper 13 | 14 | class StandardError 15 | # Displays the specific error message and the backtrace associated with it. 16 | def display 17 | "#{message}\n\t#{backtrace.join("\n\t")}" 18 | end 19 | end # class StandardError 20 | -------------------------------------------------------------------------------- /lib/data_mapper/support/inflection.rb: -------------------------------------------------------------------------------- 1 | # The original of this file was copied for the ActiveSupport project which is 2 | # part of the Ruby On Rails web-framework (http://rubyonrails.org) 3 | # 4 | # Methods have been modified or removed. English inflection is now provided via 5 | # the english gem (http://english.rubyforge.org) 6 | # 7 | # sudo gem install english 8 | # 9 | gem 'english', '>=0.2.0' 10 | require 'english/inflect' 11 | 12 | English::Inflect.word 'postgres' 13 | 14 | module DataMapper 15 | module Inflection 16 | class << self 17 | # Take an underscored name and make it into a camelized name 18 | # 19 | # Examples 20 | # "egg_and_hams".classify #=> "EggAndHam" 21 | # "post".classify #=> "Post" 22 | # 23 | def classify(name) 24 | camelize(singularize(name.to_s.sub(/.*\./, ''))) 25 | end 26 | 27 | # By default, camelize converts strings to UpperCamelCase. 28 | # 29 | # camelize will also convert '/' to '::' which is useful for converting paths to namespaces 30 | # 31 | # Examples 32 | # "active_record".camelize #=> "ActiveRecord" 33 | # "active_record/errors".camelize #=> "ActiveRecord::Errors" 34 | # 35 | def camelize(lower_case_and_underscored_word, *args) 36 | lower_case_and_underscored_word.to_s.gsub(/\/(.?)/) { "::" + $1.upcase }.gsub(/(^|_)(.)/) { $2.upcase } 37 | end 38 | 39 | 40 | # The reverse of +camelize+. Makes an underscored form from the expression in the string. 41 | # 42 | # Changes '::' to '/' to convert namespaces to paths. 43 | # 44 | # Examples 45 | # "ActiveRecord".underscore #=> "active_record" 46 | # "ActiveRecord::Errors".underscore #=> active_record/errors 47 | # 48 | def underscore(camel_cased_word) 49 | camel_cased_word.to_s.gsub(/::/, '/'). 50 | gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). 51 | gsub(/([a-z\d])([A-Z])/,'\1_\2'). 52 | tr("-", "_"). 53 | downcase 54 | end 55 | 56 | # Capitalizes the first word and turns underscores into spaces and strips _id. 57 | # Like titleize, this is meant for creating pretty output. 58 | # 59 | # Examples 60 | # "employee_salary" #=> "Employee salary" 61 | # "author_id" #=> "Author" 62 | # 63 | def humanize(lower_case_and_underscored_word) 64 | lower_case_and_underscored_word.to_s.gsub(/_id$/, "").gsub(/_/, " ").capitalize 65 | end 66 | 67 | # Removes the module part from the expression in the string 68 | # 69 | # Examples 70 | # "ActiveRecord::CoreExtensions::String::Inflections".demodulize #=> "Inflections" 71 | # "Inflections".demodulize #=> "Inflections" 72 | def demodulize(class_name_in_module) 73 | class_name_in_module.to_s.gsub(/^.*::/, '') 74 | end 75 | 76 | # Create the name of a table like Rails does for models to table names. This method 77 | # uses the pluralize method on the last word in the string. 78 | # 79 | # Examples 80 | # "RawScaledScorer".tableize #=> "raw_scaled_scorers" 81 | # "egg_and_ham".tableize #=> "egg_and_hams" 82 | # "fancyCategory".tableize #=> "fancy_categories" 83 | def tableize(class_name) 84 | pluralize(underscore(class_name)) 85 | end 86 | 87 | # Creates a foreign key name from a class name. 88 | # 89 | # Examples 90 | # "Message".foreign_key #=> "message_id" 91 | # "Admin::Post".foreign_key #=> "post_id" 92 | def foreign_key(class_name, key = "id") 93 | underscore(demodulize(class_name.to_s)) << "_" << key.to_s 94 | end 95 | 96 | # Constantize tries to find a declared constant with the name specified 97 | # in the string. It raises a NameError when the name is not in CamelCase 98 | # or is not initialized. 99 | # 100 | # Examples 101 | # "Module".constantize #=> Module 102 | # "Class".constantize #=> Class 103 | def constantize(camel_cased_word) 104 | unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ camel_cased_word 105 | raise NameError, "#{camel_cased_word.inspect} is not a valid constant name!" 106 | end 107 | 108 | Object.module_eval("::#{$1}", __FILE__, __LINE__) 109 | end 110 | 111 | # The reverse of pluralize, returns the singular form of a word in a string. 112 | # Wraps the English gem 113 | # Examples 114 | # "posts".singularize #=> "post" 115 | # "octopi".singularize #=> "octopus" 116 | # "sheep".singluarize #=> "sheep" 117 | # "word".singluarize #=> "word" 118 | # "the blue mailmen".singularize #=> "the blue mailman" 119 | # "CamelOctopi".singularize #=> "CamelOctopus" 120 | # 121 | def singularize(word) 122 | word.singular 123 | end 124 | 125 | # Returns the plural form of the word in the string. 126 | # 127 | # Examples 128 | # "post".pluralize #=> "posts" 129 | # "octopus".pluralize #=> "octopi" 130 | # "sheep".pluralize #=> "sheep" 131 | # "words".pluralize #=> "words" 132 | # "the blue mailman".pluralize #=> "the blue mailmen" 133 | # "CamelOctopus".pluralize #=> "CamelOctopi" 134 | # 135 | def pluralize(word) 136 | word.plural 137 | end 138 | 139 | end 140 | end # module Inflection 141 | end # module DataMapper 142 | -------------------------------------------------------------------------------- /lib/data_mapper/support/kernel.rb: -------------------------------------------------------------------------------- 1 | module Kernel 2 | # Delegates to DataMapper::repository. 3 | # Will not overwrite if a method of the same name is pre-defined. 4 | def repository(*args, &block) 5 | DataMapper.repository(*args, &block) 6 | end 7 | end # module Kernel 8 | -------------------------------------------------------------------------------- /lib/data_mapper/support/object.rb: -------------------------------------------------------------------------------- 1 | class Object 2 | @nested_constants = Hash.new do |h,k| 3 | klass = Object 4 | k.split('::').each do |c| 5 | klass = klass.const_get(c) 6 | end 7 | h[k] = klass 8 | end 9 | 10 | def self.recursive_const_get(nested_name) 11 | @nested_constants[nested_name] 12 | end 13 | 14 | unless instance_methods.include?('instance_variable_defined?') 15 | def instance_variable_defined?(method) 16 | instance_variables.include?(method.to_s) 17 | end 18 | end 19 | end # class Object 20 | -------------------------------------------------------------------------------- /lib/data_mapper/support/pathname.rb: -------------------------------------------------------------------------------- 1 | class Pathname 2 | def /(path) 3 | (self + path).expand_path 4 | end 5 | end # class Pathname 6 | -------------------------------------------------------------------------------- /lib/data_mapper/support/string.rb: -------------------------------------------------------------------------------- 1 | class String 2 | # Overwrite this method to provide your own translations. 3 | def self.translate(value) 4 | translations[value] || value 5 | end 6 | 7 | def self.translations 8 | @translations ||= {} 9 | end 10 | 11 | # Matches any whitespace (including newline) and replaces with a single space 12 | # EXAMPLE: 13 | # < "SELECT name FROM users" 18 | def compress_lines(spaced = true) 19 | split($/).map { |line| line.strip }.join(spaced ? ' ' : '') 20 | end 21 | 22 | # Useful for heredocs - removes whitespace margin. 23 | def margin(indicator = nil) 24 | lines = self.dup.split($/) 25 | 26 | min_margin = 0 27 | lines.each do |line| 28 | if line =~ /^(\s+)/ && (min_margin == 0 || $1.size < min_margin) 29 | min_margin = $1.size 30 | end 31 | end 32 | lines.map { |line| line.sub(/^\s{#{min_margin}}/, '') }.join($/) 33 | end 34 | 35 | # Formats String for easy translation. Replaces an arbitrary number of 36 | # values using numeric identifier replacement. 37 | # 38 | # "%s %s %s" % %w(one two three) #=> "one two three" 39 | # "%3$s %2$s %1$s" % %w(one two three) #=> "three two one" 40 | def t(*values) 41 | self.class::translate(self) % values 42 | end 43 | 44 | def to_class 45 | ::Object::recursive_const_get(self) 46 | end 47 | end # class String 48 | -------------------------------------------------------------------------------- /lib/data_mapper/support/struct.rb: -------------------------------------------------------------------------------- 1 | class Struct 2 | # Returns a hash containing the names and values for all instance variables in the Struct. 3 | def attributes 4 | h = {} 5 | each_pair { |k,v| h[k] = v } 6 | h 7 | end 8 | end # class Struct 9 | -------------------------------------------------------------------------------- /lib/data_mapper/support/symbol.rb: -------------------------------------------------------------------------------- 1 | class Symbol 2 | def gt 3 | DataMapper::Query::Operator.new(self, :gt) 4 | end 5 | 6 | def gte 7 | DataMapper::Query::Operator.new(self, :gte) 8 | end 9 | 10 | def lt 11 | DataMapper::Query::Operator.new(self, :lt) 12 | end 13 | 14 | def lte 15 | DataMapper::Query::Operator.new(self, :lte) 16 | end 17 | 18 | def not 19 | DataMapper::Query::Operator.new(self, :not) 20 | end 21 | 22 | def eql 23 | DataMapper::Query::Operator.new(self, :eql) 24 | end 25 | 26 | def like 27 | DataMapper::Query::Operator.new(self, :like) 28 | end 29 | 30 | def in 31 | DataMapper::Query::Operator.new(self, :in) 32 | end 33 | 34 | def to_proc 35 | lambda { |value| value.send(self) } 36 | end 37 | end # class Symbol 38 | -------------------------------------------------------------------------------- /lib/data_mapper/type.rb: -------------------------------------------------------------------------------- 1 | module DataMapper 2 | 3 | # :include:/QUICKLINKS 4 | # 5 | # = Types 6 | # Provides means of writing custom types for properties. Each type is based 7 | # on a ruby primitive and handles its own serialization and materialization, 8 | # and therefore is responsible for providing those methods. 9 | # 10 | # To see complete list of supported types, see documentation for 11 | # DataMapper::Property::TYPES 12 | # 13 | # == Defining new Types 14 | # To define a new type, subclass DataMapper::Type, pick ruby primitive, and 15 | # set the options for this type. 16 | # 17 | # class MyType < DataMapper::Type 18 | # primitive String 19 | # size 10 20 | # end 21 | # 22 | # Following this, you will be able to use MyType as a type for any given 23 | # property. If special materialization and serialization is required, 24 | # override the class methods 25 | # 26 | # class MyType < DataMapper::Type 27 | # primitive String 28 | # size 10 29 | # 30 | # def self.dump(value, property) 31 | # 32 | # end 33 | # 34 | # def self.load(value) 35 | # 36 | # end 37 | # end 38 | class Type 39 | PROPERTY_OPTIONS = [ 40 | :public, :protected, :private, :accessor, :reader, :writer, 41 | :lazy, :default, :nullable, :key, :serial, :field, :size, :length, 42 | :format, :index, :check, :ordinal, :auto_validation, :validates, :unique, 43 | :lock, :track 44 | ] 45 | 46 | PROPERTY_OPTION_ALIASES = { 47 | :size => [ :length ] 48 | } 49 | 50 | class << self 51 | 52 | def configure(primitive_type, options) 53 | @_primitive_type = primitive_type 54 | @_options = options 55 | 56 | def self.inherited(base) 57 | base.primitive @_primitive_type 58 | 59 | @_options.each do |k, v| 60 | base.send(k, v) 61 | end 62 | end 63 | 64 | self 65 | end 66 | 67 | # The Ruby primitive type to use as basis for this type. See 68 | # DataMapper::Property::TYPES for list of types. 69 | # 70 | # ==== Parameters 71 | # primitive:: 72 | # The class for the primitive. If nil is passed in, it returns the 73 | # current primitive 74 | # 75 | # ==== Returns 76 | # Class:: if the param is nil, return the current primitive. 77 | # 78 | # @public 79 | def primitive(primitive = nil) 80 | return @primitive if primitive.nil? 81 | 82 | @primitive = primitive 83 | end 84 | 85 | #load DataMapper::Property options 86 | PROPERTY_OPTIONS.each do |property_option| 87 | self.class_eval <<-EOS, __FILE__, __LINE__ 88 | def #{property_option}(arg = nil) 89 | return @#{property_option} if arg.nil? 90 | 91 | @#{property_option} = arg 92 | end 93 | EOS 94 | end 95 | 96 | #create property aliases 97 | PROPERTY_OPTION_ALIASES.each do |property_option, aliases| 98 | aliases.each do |ali| 99 | self.class_eval <<-EOS, __FILE__, __LINE__ 100 | alias #{ali} #{property_option} 101 | EOS 102 | end 103 | end 104 | 105 | # Gives all the options set on this type 106 | # 107 | # ==== Returns 108 | # Hash:: with all options and their values set on this type 109 | # 110 | # @public 111 | def options 112 | options = {} 113 | PROPERTY_OPTIONS.each do |method| 114 | next if (value = send(method)).nil? 115 | options[method] = value 116 | end 117 | options 118 | end 119 | end 120 | 121 | # Stub instance method for dumping 122 | # 123 | # ==== Parameters 124 | # value:: 125 | # The value to dump 126 | # property:: 127 | # The property the type is being used by 128 | # 129 | # ==== Returns 130 | # Object:: Dumped object 131 | # 132 | # 133 | # @public 134 | def self.dump(value, property) 135 | value 136 | end 137 | 138 | # Stub instance method for loading 139 | # 140 | # ==== Parameters 141 | # value:: 142 | # The value to serialize 143 | # property:: 144 | # The property the type is being used by 145 | # 146 | # ==== Returns 147 | # Object:: Serialized object. Must be the same type as the ruby primitive 148 | # 149 | # 150 | # @public 151 | def self.load(value, property) 152 | value 153 | end 154 | 155 | end # class Type 156 | 157 | def self.Type(primitive_type, options = {}) 158 | Class.new(Type).configure(primitive_type, options) 159 | end 160 | 161 | end # module DataMapper 162 | -------------------------------------------------------------------------------- /lib/data_mapper/types.rb: -------------------------------------------------------------------------------- 1 | dir = Pathname(__FILE__).dirname.expand_path / 'types' 2 | 3 | require dir / 'csv' 4 | require dir / 'enum' 5 | require dir / 'flag' 6 | require dir / 'text' 7 | require dir / 'yaml' 8 | -------------------------------------------------------------------------------- /lib/data_mapper/types/csv.rb: -------------------------------------------------------------------------------- 1 | module DataMapper 2 | module Types 3 | class Csv < DataMapper::Type 4 | primitive String 5 | size 65535 6 | lazy true 7 | 8 | def self.load(value, property) 9 | case value 10 | when String then FasterCSV.parse(value) 11 | when Array then value 12 | else nil 13 | end 14 | end 15 | 16 | def self.dump(value, property) 17 | case value 18 | when Array then 19 | FasterCSV.generate do |csv| 20 | value.each { |row| csv << row } 21 | end 22 | when String then value 23 | else nil 24 | end 25 | end 26 | end # class Csv 27 | end # module Types 28 | end # module DataMapper -------------------------------------------------------------------------------- /lib/data_mapper/types/enum.rb: -------------------------------------------------------------------------------- 1 | module DataMapper 2 | module Types 3 | class Enum < DataMapper::Type(Fixnum) 4 | 5 | def self.flag_map 6 | @flag_map 7 | end 8 | 9 | def self.flag_map=(value) 10 | @flag_map = value 11 | end 12 | 13 | def self.new(*flags) 14 | enum = Enum.dup 15 | enum.flag_map = {} 16 | 17 | flags.each_with_index do |flag, i| 18 | enum.flag_map[i + 1] = flag 19 | end 20 | 21 | enum 22 | end 23 | 24 | def self.[](*flags) 25 | new(*flags) 26 | end 27 | 28 | def self.load(value) 29 | self.flag_map[value] 30 | end 31 | 32 | def self.dump(flag) 33 | self.flag_map.invert[flag] 34 | end 35 | end 36 | end 37 | end -------------------------------------------------------------------------------- /lib/data_mapper/types/flag.rb: -------------------------------------------------------------------------------- 1 | module DataMapper 2 | module Types 3 | class Flag < DataMapper::Type(Fixnum) 4 | 5 | def self.flag_map 6 | @flag_map 7 | end 8 | 9 | def self.flag_map=(value) 10 | @flag_map = value 11 | end 12 | 13 | def self.new(*flags) 14 | type = Flag.dup 15 | type.flag_map = {} 16 | 17 | flags.each_with_index do |flag, i| 18 | type.flag_map[2 ** i] = flag 19 | end 20 | 21 | type 22 | end 23 | 24 | def self.[](*flags) 25 | new(*flags) 26 | end 27 | 28 | def self.load(value) 29 | begin 30 | matches = [] 31 | 32 | 0.upto((Math.log(value) / Math.log(2)).ceil) do |i| 33 | pow = 2 ** i 34 | matches << flag_map[pow] if value & pow == pow 35 | end 36 | 37 | matches.compact 38 | rescue TypeError, Errno::EDOM 39 | [] 40 | end 41 | end 42 | 43 | def self.dump(*flags) 44 | flag_map.invert.values_at(*flags.flatten).compact.inject(0) {|sum, i| sum + i} 45 | end 46 | end 47 | end 48 | end -------------------------------------------------------------------------------- /lib/data_mapper/types/text.rb: -------------------------------------------------------------------------------- 1 | # FIXME: can we alias this to the class Text if it isn't already defined? 2 | module DataMapper 3 | module Types 4 | class Text < DataMapper::Type 5 | primitive String 6 | size 65535 7 | lazy true 8 | end # class Text 9 | end # module Types 10 | end # module DataMapper -------------------------------------------------------------------------------- /lib/data_mapper/types/yaml.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | module DataMapper 4 | module Types 5 | class Yaml < DataMapper::Type 6 | primitive String 7 | size 65535 8 | lazy true 9 | 10 | def self.load(value, property) 11 | if value.nil? 12 | nil 13 | elsif value.is_a?(String) 14 | ::YAML.load(value) 15 | else 16 | raise ArgumentError.new("+value+ must be nil or a String") 17 | end 18 | end 19 | 20 | def self.dump(value, property) 21 | if value.nil? 22 | nil 23 | elsif value.is_a?(String) && value =~ /^---/ 24 | value 25 | else 26 | ::YAML.dump(value) 27 | end 28 | end 29 | end # class Yaml 30 | end # module Types 31 | end # module DataMapper -------------------------------------------------------------------------------- /script/all: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | ADAPTER=sqlite3 rake 3 | ADAPTER=mysql rake 4 | ADAPTER=postgresql rake -------------------------------------------------------------------------------- /script/profile.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require File.join(File.dirname(__FILE__), '..', 'lib', 'data_mapper') 4 | 5 | require 'ruby-prof' 6 | 7 | OUTPUT = DataMapper.root / 'profile_results.txt' 8 | 9 | SOCKET_FILE = Pathname.glob(%w[ 10 | /opt/local/var/run/mysql5/mysqld.sock 11 | tmp/mysqld.sock 12 | tmp/mysql.sock 13 | ]).find(&:socket?) 14 | 15 | DataMapper::Logger.new(DataMapper.root / 'log' / 'dm.log', :debug) 16 | DataMapper.setup(:default, "mysql://root@localhost/data_mapper_1?socket=#{SOCKET_FILE}") 17 | 18 | class Exhibit 19 | include DataMapper::Resource 20 | 21 | property :id, Fixnum, :serial => true 22 | property :name, String 23 | property :zoo_id, Fixnum 24 | property :notes, String, :lazy => true 25 | property :created_on, Date 26 | property :updated_at, DateTime 27 | 28 | end 29 | 30 | touch_attributes = lambda do |exhibits| 31 | [*exhibits].each do |exhibit| 32 | exhibit.id 33 | exhibit.name 34 | exhibit.created_on 35 | exhibit.updated_at 36 | end 37 | end 38 | 39 | # RubyProf, making profiling Ruby pretty since 1899! 40 | def profile(&b) 41 | result = RubyProf.profile &b 42 | printer = RubyProf::FlatPrinter.new(result) 43 | printer.print(OUTPUT.open('w+')) 44 | end 45 | 46 | profile do 47 | 10_000.times { touch_attributes[Exhibit.get(1)] } 48 | 49 | # repository(:default) do 50 | # 10_000.times { touch_attributes[Exhibit.get(1)] } 51 | # end 52 | # 53 | # 1000.times { touch_attributes[Exhibit.all(:limit => 100)] } 54 | # 55 | # repository(:default) do 56 | # 1000.times { touch_attributes[Exhibit.all(:limit => 100)] } 57 | # end 58 | # 59 | # 10.times { touch_attributes[Exhibit.all(:limit => 10_000)] } 60 | # 61 | # repository(:default) do 62 | # 10.times { touch_attributes[Exhibit.all(:limit => 10_000)] } 63 | # end 64 | end 65 | 66 | puts "Done!" 67 | -------------------------------------------------------------------------------- /spec/integration/association_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | begin 4 | gem 'do_sqlite3', '=0.9.0' 5 | require 'do_sqlite3' 6 | 7 | DataMapper.setup(:sqlite3, "sqlite3://#{INTEGRATION_DB_PATH}") 8 | 9 | class Engine 10 | include DataMapper::Resource 11 | 12 | property :id, Fixnum, :serial => true 13 | property :name, String 14 | end 15 | 16 | class Yard 17 | include DataMapper::Resource 18 | 19 | property :id, Fixnum, :serial => true 20 | property :name, String 21 | 22 | repository(:sqlite3) do 23 | many_to_one :engine 24 | end 25 | end 26 | 27 | class Pie 28 | include DataMapper::Resource 29 | 30 | property :id, Fixnum, :serial => true 31 | property :name, String 32 | end 33 | 34 | class Sky 35 | include DataMapper::Resource 36 | 37 | property :id, Fixnum, :serial => true 38 | property :name, String 39 | 40 | repository(:sqlite3) do 41 | one_to_one :pie 42 | end 43 | end 44 | 45 | class Host 46 | include DataMapper::Resource 47 | 48 | property :id, Fixnum, :serial => true 49 | property :name, String 50 | 51 | repository(:sqlite3) do 52 | one_to_many :slices 53 | end 54 | end 55 | 56 | class Slice 57 | include DataMapper::Resource 58 | 59 | property :id, Fixnum, :serial => true 60 | property :name, String 61 | 62 | repository(:sqlite3) do 63 | many_to_one :host 64 | end 65 | end 66 | 67 | describe DataMapper::Associations do 68 | describe "many to one associations" do 69 | before do 70 | @adapter = repository(:sqlite3).adapter 71 | 72 | @adapter.execute(<<-EOS.compress_lines) 73 | CREATE TABLE "engines" ( 74 | "id" INTEGER PRIMARY KEY, 75 | "name" VARCHAR(50) 76 | ) 77 | EOS 78 | 79 | @adapter.execute('INSERT INTO "engines" ("id", "name") values (?, ?)', 1, 'engine1') 80 | @adapter.execute('INSERT INTO "engines" ("id", "name") values (?, ?)', 2, 'engine2') 81 | 82 | @adapter.execute(<<-EOS.compress_lines) 83 | CREATE TABLE "yards" ( 84 | "id" INTEGER PRIMARY KEY, 85 | "name" VARCHAR(50), 86 | "engine_id" INTEGER 87 | ) 88 | EOS 89 | 90 | @adapter.execute('INSERT INTO "yards" ("id", "name", "engine_id") values (?, ?, ?)', 1, 'yard1', 1) 91 | end 92 | 93 | it "should load without the parent" 94 | 95 | it 'should allow substituting the parent' do 96 | y = repository(:sqlite3).all(Yard, :id => 1).first 97 | e = repository(:sqlite3).all(Engine, :id => 2).first 98 | 99 | y.engine = e 100 | repository(:sqlite3).save(y) 101 | 102 | y = repository(:sqlite3).all(Yard, :id => 1).first 103 | y[:engine_id].should == 2 104 | end 105 | 106 | it "#many_to_one" do 107 | yard = Yard.new 108 | yard.should respond_to(:engine) 109 | yard.should respond_to(:engine=) 110 | end 111 | 112 | it "should load the associated instance" do 113 | y = repository(:sqlite3).all(Yard, :id => 1).first 114 | y.engine.should_not be_nil 115 | y.engine.id.should == 1 116 | y.engine.name.should == "engine1" 117 | end 118 | 119 | it 'should save the association key in the child' do 120 | repository(:sqlite3) do 121 | e = repository(:sqlite3).all(Engine, :id => 2).first 122 | repository(:sqlite3).save(Yard.new(:id => 2, :name => 'yard2', :engine => e)) 123 | end 124 | 125 | repository(:sqlite3).all(Yard, :id => 2).first[:engine_id].should == 2 126 | end 127 | 128 | it 'should save the parent upon saving of child' do 129 | y = nil 130 | repository(:sqlite3) do |r| 131 | e = Engine.new(:id => 10, :name => "engine10") 132 | y = Yard.new(:id => 10, :name => "Yard10", :engine => e) 133 | r.save(y) 134 | end 135 | 136 | y[:engine_id].should == 10 137 | repository(:sqlite3).all(Engine, :id => 10).first.should_not be_nil 138 | end 139 | 140 | after do 141 | @adapter.execute('DROP TABLE "yards"') 142 | @adapter.execute('DROP TABLE "engines"') 143 | end 144 | end 145 | 146 | describe "one to one associations" do 147 | before do 148 | @adapter = repository(:sqlite3).adapter 149 | 150 | @adapter.execute(<<-EOS.compress_lines) 151 | CREATE TABLE "skies" ( 152 | "id" INTEGER PRIMARY KEY, 153 | "name" VARCHAR(50) 154 | ) 155 | EOS 156 | 157 | @adapter.execute('INSERT INTO "skies" ("id", "name") values (?, ?)', 1, 'sky1') 158 | 159 | @adapter.execute(<<-EOS.compress_lines) 160 | CREATE TABLE "pies" ( 161 | "id" INTEGER PRIMARY KEY, 162 | "name" VARCHAR(50), 163 | "sky_id" INTEGER 164 | ) 165 | EOS 166 | 167 | @adapter.execute('INSERT INTO "pies" ("id", "name", "sky_id") values (?, ?, ?)', 1, 'pie1', 1) 168 | @adapter.execute('INSERT INTO "pies" ("id", "name") values (?, ?)', 2, 'pie2') 169 | end 170 | 171 | it 'should allow substituting the child' do 172 | s = repository(:sqlite3).all(Sky, :id => 1).first 173 | p = repository(:sqlite3).all(Pie, :id => 2).first 174 | 175 | s.pie = p 176 | 177 | p1 = repository(:sqlite3).first(Pie, :id => 1) 178 | p1[:sky_id].should be_nil 179 | 180 | p2 = repository(:sqlite3).first(Pie, :id => 2) 181 | p2[:sky_id].should == 1 182 | end 183 | 184 | it "#one_to_one" do 185 | s = Sky.new 186 | s.should respond_to(:pie) 187 | s.should respond_to(:pie=) 188 | end 189 | 190 | it "should load the associated instance" do 191 | s = repository(:sqlite3).first(Sky, :id => 1) 192 | s.pie.should_not be_nil 193 | s.pie.id.should == 1 194 | s.pie.name.should == "pie1" 195 | end 196 | 197 | it 'should save the association key in the child' do 198 | repository(:sqlite3) do |r| 199 | p = r.first(Pie, :id => 2) 200 | r.save(Sky.new(:id => 2, :name => 'sky2', :pie => p)) 201 | end 202 | 203 | repository(:sqlite3).first(Pie, :id => 2)[:sky_id].should == 2 204 | end 205 | 206 | it 'should save the children upon saving of parent' do 207 | repository(:sqlite3) do |r| 208 | p = Pie.new(:id => 10, :name => "pie10") 209 | s = Sky.new(:id => 10, :name => "sky10", :pie => p) 210 | 211 | r.save(s) 212 | 213 | p[:sky_id].should == 10 214 | end 215 | 216 | repository(:sqlite3).first(Pie, :id => 10).should_not be_nil 217 | end 218 | 219 | after do 220 | @adapter.execute('DROP TABLE "pies"') 221 | @adapter.execute('DROP TABLE "skies"') 222 | end 223 | end 224 | 225 | describe "one to many associations" do 226 | before do 227 | @adapter = repository(:sqlite3).adapter 228 | 229 | @adapter.execute(<<-EOS.compress_lines) 230 | CREATE TABLE "hosts" ( 231 | "id" INTEGER PRIMARY KEY, 232 | "name" VARCHAR(50) 233 | ) 234 | EOS 235 | 236 | @adapter.execute('INSERT INTO "hosts" ("id", "name") values (?, ?)', 1, 'host1') 237 | @adapter.execute('INSERT INTO "hosts" ("id", "name") values (?, ?)', 2, 'host2') 238 | 239 | @adapter.execute(<<-EOS.compress_lines) 240 | CREATE TABLE "slices" ( 241 | "id" INTEGER PRIMARY KEY, 242 | "name" VARCHAR(50), 243 | "host_id" INTEGER 244 | ) 245 | EOS 246 | 247 | @adapter.execute('INSERT INTO "slices" ("id", "name", "host_id") values (?, ?, ?)', 1, 'slice1', 1) 248 | @adapter.execute('INSERT INTO "slices" ("id", "name", "host_id") values (?, ?, ?)', 2, 'slice2', 1) 249 | end 250 | 251 | it "#one_to_many" do 252 | h = Host.new 253 | h.should respond_to(:slices) 254 | end 255 | 256 | it "should allow removal of a child through a loaded association" do 257 | h = repository(:sqlite3).all(Host, :id => 1).first 258 | s = h.slices.first 259 | 260 | h.slices.delete(s) 261 | h.slices.size.should == 1 262 | 263 | s = repository(:sqlite3).first(Slice, :id => s.id) 264 | s.host.should be_nil 265 | s[:host_id].should be_nil 266 | end 267 | 268 | it "should load the associated instances" do 269 | h = repository(:sqlite3).all(Host, :id => 1).first 270 | h.slices.should_not be_nil 271 | h.slices.size.should == 2 272 | h.slices.first.id.should == 1 273 | h.slices.last.id.should == 2 274 | end 275 | 276 | it "should add and save the associated instance" do 277 | h = repository(:sqlite3).all(Host, :id => 1).first 278 | h.slices << Slice.new(:id => 3, :name => 'slice3') 279 | 280 | s = repository(:sqlite3).all(Slice, :id => 3).first 281 | s.host.id.should == 1 282 | end 283 | 284 | it "should not save the associated instance if the parent is not saved" do 285 | repository(:sqlite3) do 286 | h = Host.new(:id => 10, :name => "host10") 287 | h.slices << Slice.new(:id => 10, :name => 'slice10') 288 | end 289 | 290 | repository(:sqlite3).all(Slice, :id => 10).first.should be_nil 291 | end 292 | 293 | it "should save the associated instance upon saving of parent" do 294 | repository(:sqlite3) do |r| 295 | h = Host.new(:id => 10, :name => "host10") 296 | h.slices << Slice.new(:id => 10, :name => 'slice10') 297 | r.save(h) 298 | end 299 | 300 | s = repository(:sqlite3).all(Slice, :id => 10).first 301 | s.should_not be_nil 302 | s.host.should_not be_nil 303 | s.host.id.should == 10 304 | end 305 | 306 | # describe '#through' do 307 | # before(:all) do 308 | # class Cake 309 | # property :id, Fixnum, :serial => true 310 | # property :name, String 311 | # has :slices, 1..n 312 | # end 313 | # 314 | # @adapter.execute(<<-EOS.compress_lines) 315 | # CREATE TABLE "cakes" ( 316 | # "id" INTEGER PRIMARY KEY, 317 | # "name" VARCHAR(50) 318 | # ) 319 | # EOS 320 | # 321 | # @adapter.execute('INSERT INTO "cakes" ("id", "name") values (?, ?)', 1, 'cake1', 1) 322 | # @adapter.execute('INSERT INTO "cakes" ("id", "name") values (?, ?)', 2, 'cake2', 1) 323 | # 324 | # class Slice 325 | # has :cake, n..1 326 | # end 327 | # end 328 | # 329 | # end 330 | 331 | after do 332 | @adapter.execute('DROP TABLE "slices"') 333 | @adapter.execute('DROP TABLE "hosts"') 334 | end 335 | end 336 | end 337 | rescue LoadError 338 | warn "integration/association_spec not run! Could not load do_sqlite3." 339 | end 340 | -------------------------------------------------------------------------------- /spec/integration/data_objects_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | gem 'do_sqlite3', '=0.9.0' 4 | require 'do_sqlite3' 5 | 6 | describe DataMapper::Adapters::DataObjectsAdapter do 7 | 8 | describe "when using transactions" do 9 | 10 | before :each do 11 | @adapter = DataMapper::Adapters::Sqlite3Adapter.new(:sqlite3, URI.parse("sqlite3://#{INTEGRATION_DB_PATH}")) 12 | @transaction = DataMapper::Adapters::Transaction.new(@adapter) 13 | @transaction.begin 14 | end 15 | 16 | describe "#close_connection" do 17 | it "should not close connections that are used for the current transaction" do 18 | @transaction.connection_for(@adapter).should_not_receive(:close) 19 | @transaction.within do 20 | @adapter.close_connection(@transaction.connection_for(@adapter)) 21 | end 22 | end 23 | it "should still close connections that are not used for the current transaction" do 24 | conn2 = mock("connection2") 25 | conn2.should_receive(:close) 26 | @transaction.within do 27 | @adapter.close_connection(conn2) 28 | end 29 | end 30 | end 31 | it "should return a fresh connection on #create_connection_outside_transaction" do 32 | DataObjects::Connection.should_receive(:new).once.with(@adapter.uri) 33 | conn = @adapter.create_connection_outside_transaction 34 | end 35 | describe "#create_connection" do 36 | it "should return the connection for the transaction if within a transaction" do 37 | @transaction.within do 38 | @adapter.create_connection.should == @transaction.connection_for(@adapter) 39 | end 40 | end 41 | it "should return new connections if not within a transaction" do 42 | @adapter.create_connection.should_not == @transaction.connection_for(@adapter) 43 | end 44 | end 45 | end 46 | 47 | end 48 | -------------------------------------------------------------------------------- /spec/integration/mysql_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | begin 4 | gem 'do_mysql', '=0.9.0' 5 | require 'do_mysql' 6 | 7 | DataMapper.setup(:mysql, "mysql://localhost/dm_core_test") 8 | 9 | describe DataMapper::Adapters::DataObjectsAdapter do 10 | before :all do 11 | @adapter = repository(:mysql).adapter 12 | end 13 | 14 | describe "handling transactions" do 15 | before :all do 16 | @adapter.execute('DROP TABLE IF EXISTS sputniks') 17 | @adapter.execute('CREATE TABLE sputniks (id serial, name text) ENGINE = innodb') 18 | end 19 | 20 | before :each do 21 | @transaction = DataMapper::Adapters::Transaction.new(@adapter) 22 | end 23 | 24 | it "should rollback changes when #rollback_transaction is called" do 25 | @transaction.commit do |trans| 26 | @adapter.execute("INSERT INTO sputniks (name) VALUES ('my pretty sputnik')") 27 | trans.rollback 28 | end 29 | @adapter.query("SELECT * FROM sputniks WHERE name = 'my pretty sputnik'").empty?.should == true 30 | end 31 | it "should commit changes when #commit_transaction is called" do 32 | @transaction.commit do 33 | @adapter.execute("INSERT INTO sputniks (name) VALUES ('my pretty sputnik')") 34 | end 35 | @adapter.query("SELECT * FROM sputniks WHERE name = 'my pretty sputnik'").size.should == 1 36 | end 37 | end 38 | 39 | end 40 | rescue LoadError => e 41 | describe 'do_mysql' do 42 | it 'should be required' do 43 | fail "MySQL integration specs not run! Could not load do_mysql: #{e}" 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/integration/property_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | gem 'fastercsv', '>=1.2.3' 4 | require 'fastercsv' 5 | 6 | begin 7 | gem 'do_sqlite3', '=0.9.0' 8 | require 'do_sqlite3' 9 | 10 | DataMapper.setup(:sqlite3, "sqlite3://#{INTEGRATION_DB_PATH}") 11 | 12 | describe DataMapper::Property do 13 | before do 14 | @adapter = repository(:sqlite3).adapter 15 | end 16 | 17 | describe" tracking strategies" do 18 | before do 19 | class Actor 20 | include DataMapper::Resource 21 | 22 | property :id, Fixnum, :serial => true 23 | property :name, String, :lock => true 24 | property :notes, DataMapper::Types::Text, :track => false 25 | property :age, Fixnum, :track => :set 26 | property :rating, Fixnum # :track default should be false for immutable types 27 | property :location, String # :track default should be :get for mutable types 28 | property :lead, TrueClass, :track => :load 29 | property :agent, String, :track => :hash # :track only Object#hash value on :load. 30 | # Potentially faster, but less safe, so use judiciously, when the odds of a hash-collision are low. 31 | end 32 | 33 | @adapter.execute <<-EOS.compress_lines 34 | CREATE TABLE actors ( 35 | id INTEGER PRIMARY KEY, 36 | name TEXT, 37 | notes TEXT, 38 | age INTEGER, 39 | rating INTEGER, 40 | location TEXT, 41 | lead BOOLEAN, 42 | agent TEXT 43 | ) 44 | EOS 45 | end 46 | 47 | it "false" do 48 | pending("Implementation...") 49 | DataMapper::Resource::DIRTY.should_not be_nil 50 | bob = Actor.new(:name => 'bob') 51 | bob.original_attributes.should have_key(:name) 52 | bob.original_attributes[:name].should == DataMapper::Resource::DIRTY 53 | end 54 | 55 | it ":load" do 56 | pending("Implementation...") 57 | DataMapper::Resource::DIRTY.should_not be_nil 58 | bob = Actor.new(:name => 'bob') 59 | bob.original_attributes.should have_key(:name) 60 | bob.original_attributes[:name].should == DataMapper::Resource::DIRTY 61 | end 62 | 63 | it ":hash" do 64 | pending("Implementation...") 65 | DataMapper::Resource::DIRTY.should_not be_nil 66 | bob = Actor.new(:name => 'bob') 67 | bob.original_attributes.should have_key(:name) 68 | bob.original_attributes[:name].should == DataMapper::Resource::DIRTY 69 | end 70 | 71 | it ":get" do 72 | pending("Implementation...") 73 | DataMapper::Resource::DIRTY.should_not be_nil 74 | bob = Actor.new(:name => 'bob') 75 | bob.original_attributes.should have_key(:name) 76 | bob.original_attributes[:name].should == DataMapper::Resource::DIRTY 77 | end 78 | 79 | it ":set" do 80 | pending("Implementation...") 81 | DataMapper::Resource::DIRTY.should_not be_nil 82 | bob = Actor.new(:name => 'bob') 83 | bob.original_attributes.should have_key(:name) 84 | bob.original_attributes[:name].should == DataMapper::Resource::DIRTY 85 | end 86 | 87 | after do 88 | @adapter.execute("DROP TABLE actors") 89 | end 90 | end 91 | 92 | describe "lazy loading" do 93 | before do 94 | @adapter.execute(<<-EOS.compress_lines) rescue nil 95 | CREATE TABLE "sail_boats" ( 96 | "id" INTEGER PRIMARY KEY, 97 | "notes" VARCHAR(50), 98 | "trip_report" VARCHAR(50), 99 | "miles" INTEGER 100 | ) 101 | EOS 102 | 103 | class SailBoat 104 | include DataMapper::Resource 105 | property :id, Fixnum, :serial => true 106 | property :notes, String, :lazy => [:notes] 107 | property :trip_report, String, :lazy => [:notes,:trip] 108 | property :miles, Fixnum, :lazy => [:trip] 109 | 110 | class << self 111 | def property_by_name(name) 112 | properties(repository.name)[name] 113 | end 114 | end 115 | end 116 | repository(:sqlite3).save(SailBoat.new(:id => 1, :notes=>'Note',:trip_report=>'Report',:miles=>23)) 117 | repository(:sqlite3).save(SailBoat.new(:id => 2, :notes=>'Note',:trip_report=>'Report',:miles=>23)) 118 | repository(:sqlite3).save(SailBoat.new(:id => 3, :notes=>'Note',:trip_report=>'Report',:miles=>23)) 119 | end 120 | 121 | 122 | it "should lazy load in context" do 123 | result = repository(:sqlite3).all(SailBoat,{}) 124 | result[0].instance_variables.should_not include('@notes') 125 | result[0].instance_variables.should_not include('@trip_report') 126 | result[1].instance_variables.should_not include('@notes') 127 | result[0].notes.should_not be_nil 128 | result[1].instance_variables.should include('@notes') 129 | result[1].instance_variables.should include('@trip_report') 130 | result[1].instance_variables.should_not include('@miles') 131 | 132 | result = repository(:sqlite3).all(SailBoat,{}) 133 | result[0].instance_variables.should_not include('@trip_report') 134 | result[0].instance_variables.should_not include('@miles') 135 | 136 | result[1].trip_report.should_not be_nil 137 | result[2].instance_variables.should include('@miles') 138 | end 139 | 140 | after do 141 | @adapter.execute('DROP TABLE "sail_boats"') 142 | end 143 | 144 | end 145 | 146 | end 147 | 148 | rescue LoadError 149 | warn "integration/property_spec not run! Could not load do_sqlite3." 150 | end 151 | -------------------------------------------------------------------------------- /spec/integration/repository_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | begin 4 | gem 'do_sqlite3', '=0.9.0' 5 | require 'do_sqlite3' 6 | 7 | DataMapper.setup(:sqlite3, "sqlite3://#{INTEGRATION_DB_PATH}") 8 | 9 | describe DataMapper::Repository do 10 | describe "finders" do 11 | before do 12 | class SerialFinderSpec 13 | include DataMapper::Resource 14 | 15 | property :id, Fixnum, :serial => true 16 | property :sample, String 17 | end 18 | 19 | @adapter = repository(:sqlite3).adapter 20 | 21 | @adapter.execute(<<-EOS.compress_lines) 22 | CREATE TABLE "serial_finder_specs" ( 23 | "id" INTEGER PRIMARY KEY, 24 | "sample" VARCHAR(50) 25 | ) 26 | EOS 27 | 28 | # Why do we keep testing with Repository instead of the models directly? 29 | # Just because we're trying to target the code we're actualling testing 30 | # as much as possible. 31 | setup_repository = repository(:sqlite3) 32 | 100.times do 33 | setup_repository.save(SerialFinderSpec.new(:sample => rand.to_s)) 34 | end 35 | end 36 | 37 | it "should return all available rows" do 38 | repository(:sqlite3).all(SerialFinderSpec, {}).should have(100).entries 39 | end 40 | 41 | it "should allow limit and offset" do 42 | repository(:sqlite3).all(SerialFinderSpec, { :limit => 50 }).should have(50).entries 43 | 44 | repository(:sqlite3).all(SerialFinderSpec, { :limit => 20, :offset => 40 }).map(&:id).should == 45 | repository(:sqlite3).all(SerialFinderSpec, {})[40...60].map(&:id) 46 | end 47 | 48 | it "should lazy-load missing attributes" do 49 | sfs = repository(:sqlite3).all(SerialFinderSpec, { :fields => [:id], :limit => 1 }).first 50 | sfs.should be_a_kind_of(SerialFinderSpec) 51 | sfs.should_not be_a_new_record 52 | 53 | sfs.instance_variables.should_not include('@sample') 54 | sfs.sample.should_not be_nil 55 | end 56 | 57 | it "should translate an Array to an IN clause" do 58 | ids = repository(:sqlite3).all(SerialFinderSpec, { :limit => 10 }).map(&:id) 59 | results = repository(:sqlite3).all(SerialFinderSpec, { :id => ids }) 60 | 61 | results.size.should == 10 62 | results.map(&:id).should == ids 63 | end 64 | 65 | after do 66 | @adapter.execute('DROP TABLE "serial_finder_specs"') 67 | end 68 | 69 | end 70 | end 71 | rescue LoadError 72 | warn "integration/repository_spec not run! Could not load do_sqlite3." 73 | end 74 | -------------------------------------------------------------------------------- /spec/integration/sqlite3_adapter_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | begin 4 | gem 'do_sqlite3', '=0.9.0' 5 | require 'do_sqlite3' 6 | 7 | DataMapper.setup(:sqlite3, "sqlite3://#{INTEGRATION_DB_PATH}") 8 | 9 | describe DataMapper::Adapters::DataObjectsAdapter do 10 | 11 | describe "handling transactions" do 12 | before :all do 13 | @adapter = repository(:sqlite3).adapter 14 | @adapter.execute('DROP TABLE IF EXISTS "sputniks"') 15 | @adapter.execute('CREATE TABLE "sputniks" (id serial, name text)') 16 | end 17 | 18 | before :each do 19 | @transaction = DataMapper::Adapters::Transaction.new(@adapter) 20 | end 21 | 22 | it "should rollback changes when #rollback_transaction is called" do 23 | @transaction.commit do |transaction| 24 | @adapter.execute("INSERT INTO sputniks (name) VALUES ('my pretty sputnik')") 25 | transaction.rollback 26 | end 27 | @adapter.query("SELECT * FROM sputniks WHERE name = 'my pretty sputnik'").empty?.should == true 28 | end 29 | it "should commit changes when #commit_transaction is called" do 30 | @transaction.commit do 31 | @adapter.execute("INSERT INTO sputniks (name) VALUES ('my pretty sputnik')") 32 | end 33 | @adapter.query("SELECT * FROM sputniks WHERE name = 'my pretty sputnik'").size.should == 1 34 | end 35 | end 36 | 37 | describe "reading & writing a database" do 38 | 39 | before do 40 | @adapter = repository(:sqlite3).adapter 41 | @adapter.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT)") 42 | @adapter.execute("INSERT INTO users (name) VALUES ('Paul')") 43 | end 44 | 45 | it 'should be able to #execute an arbitrary query' do 46 | result = @adapter.execute("INSERT INTO users (name) VALUES ('Sam')") 47 | 48 | result.affected_rows.should == 1 49 | end 50 | 51 | it 'should be able to #query' do 52 | result = @adapter.query("SELECT * FROM users") 53 | 54 | result.should be_kind_of(Array) 55 | row = result.first 56 | row.should be_kind_of(Struct) 57 | row.members.should == %w{id name} 58 | 59 | row.id.should == 1 60 | row.name.should == 'Paul' 61 | end 62 | 63 | it 'should return an empty array if #query found no rows' do 64 | @adapter.execute("DELETE FROM users") 65 | 66 | result = nil 67 | lambda { result = @adapter.query("SELECT * FROM users") }.should_not raise_error 68 | 69 | result.should be_kind_of(Array) 70 | result.size.should == 0 71 | end 72 | 73 | after do 74 | @adapter.execute('DROP TABLE "users"') 75 | end 76 | end 77 | 78 | describe "CRUD for serial Key" do 79 | before do 80 | class VideoGame 81 | include DataMapper::Resource 82 | 83 | property :id, Fixnum, :serial => true 84 | property :name, String 85 | end 86 | 87 | @adapter = repository(:sqlite3).adapter 88 | @adapter.execute('CREATE TABLE "video_games" ("id" INTEGER PRIMARY KEY, "name" VARCHAR(50))') rescue nil 89 | end 90 | 91 | it 'should be able to create a record' do 92 | game = VideoGame.new(:name => 'System Shock') 93 | repository(:sqlite3).save(game) 94 | 95 | game.should_not be_a_new_record 96 | game.should_not be_dirty 97 | 98 | @adapter.query('SELECT "id" FROM "video_games" WHERE "name" = ?', game.name).first.should == game.id 99 | @adapter.execute('DELETE FROM "video_games" WHERE "id" = ?', game.id).to_i.should == 1 100 | end 101 | 102 | it 'should be able to read a record' do 103 | name = 'Wing Commander: Privateer' 104 | id = @adapter.execute('INSERT INTO "video_games" ("name") VALUES (?)', name).insert_id 105 | 106 | game = repository(:sqlite3).get(VideoGame, [id]) 107 | game.name.should == name 108 | game.should_not be_dirty 109 | game.should_not be_a_new_record 110 | 111 | @adapter.execute('DELETE FROM "video_games" WHERE "name" = ?', name) 112 | end 113 | 114 | it 'should be able to update a record' do 115 | name = 'Resistance: Fall of Mon' 116 | id = @adapter.execute('INSERT INTO "video_games" ("name") VALUES (?)', name).insert_id 117 | 118 | game = repository(:sqlite3).get(VideoGame, [id]) 119 | game.name = game.name.sub(/Mon/, 'Man') 120 | 121 | game.should_not be_a_new_record 122 | game.should be_dirty 123 | 124 | repository(:sqlite3).save(game) 125 | 126 | game.should_not be_dirty 127 | 128 | clone = repository(:sqlite3).get(VideoGame, [id]) 129 | 130 | clone.name.should == game.name 131 | 132 | @adapter.execute('DELETE FROM "video_games" WHERE "id" = ?', id) 133 | end 134 | 135 | it 'should be able to delete a record' do 136 | name = 'Zelda' 137 | id = @adapter.execute('INSERT INTO "video_games" ("name") VALUES (?)', name).insert_id 138 | 139 | game = repository(:sqlite3).get(VideoGame, [id]) 140 | game.name.should == name 141 | 142 | repository(:sqlite3).destroy(game).should be_true 143 | game.should be_a_new_record 144 | game.should be_dirty 145 | end 146 | 147 | it 'should respond to Resource#get' do 148 | name = 'Contra' 149 | id = @adapter.execute('INSERT INTO "video_games" ("name") VALUES (?)', name).insert_id 150 | 151 | contra = repository(:sqlite3) { VideoGame.get(id) } 152 | 153 | contra.should_not be_nil 154 | contra.should_not be_dirty 155 | contra.should_not be_a_new_record 156 | contra.id.should == id 157 | end 158 | 159 | after do 160 | @adapter.execute('DROP TABLE "video_games"') 161 | end 162 | end 163 | 164 | describe "CRUD for Composite Key" do 165 | before do 166 | class BankCustomer 167 | include DataMapper::Resource 168 | 169 | property :bank, String, :key => true 170 | property :account_number, String, :key => true 171 | property :name, String 172 | end 173 | 174 | @adapter = repository(:sqlite3).adapter 175 | @adapter.execute('CREATE TABLE "bank_customers" ("bank" VARCHAR(50), "account_number" VARCHAR(50), "name" VARCHAR(50))') rescue nil 176 | end 177 | 178 | it 'should be able to create a record' do 179 | customer = BankCustomer.new(:bank => 'Community Bank', :acount_number => '123456', :name => 'David Hasselhoff') 180 | repository(:sqlite3).save(customer) 181 | 182 | customer.should_not be_a_new_record 183 | customer.should_not be_dirty 184 | 185 | row = @adapter.query('SELECT "bank", "account_number" FROM "bank_customers" WHERE "name" = ?', customer.name).first 186 | row.bank.should == customer.bank 187 | row.account_number.should == customer.account_number 188 | end 189 | 190 | it 'should be able to read a record' do 191 | bank, account_number, name = 'Chase', '4321', 'Super Wonderful' 192 | @adapter.execute('INSERT INTO "bank_customers" ("bank", "account_number", "name") VALUES (?, ?, ?)', bank, account_number, name) 193 | 194 | repository(:sqlite3).get(BankCustomer, [bank, account_number]).name.should == name 195 | 196 | @adapter.execute('DELETE FROM "bank_customers" WHERE "bank" = ? AND "account_number" = ?', bank, account_number) 197 | end 198 | 199 | it 'should be able to update a record' do 200 | bank, account_number, name = 'Wells Fargo', '00101001', 'Spider Pig' 201 | @adapter.execute('INSERT INTO "bank_customers" ("bank", "account_number", "name") VALUES (?, ?, ?)', bank, account_number, name) 202 | 203 | customer = repository(:sqlite3).get(BankCustomer, [bank, account_number]) 204 | customer.name = 'Bat-Pig' 205 | 206 | customer.should_not be_a_new_record 207 | customer.should be_dirty 208 | 209 | repository(:sqlite3).save(customer) 210 | 211 | customer.should_not be_dirty 212 | 213 | clone = repository(:sqlite3).get(BankCustomer, [bank, account_number]) 214 | 215 | clone.name.should == customer.name 216 | 217 | @adapter.execute('DELETE FROM "bank_customers" WHERE "bank" = ? AND "account_number" = ?', bank, account_number) 218 | end 219 | 220 | it 'should be able to delete a record' do 221 | bank, account_number, name = 'Megacorp', 'ABC', 'Flash Gordon' 222 | @adapter.execute('INSERT INTO "bank_customers" ("bank", "account_number", "name") VALUES (?, ?, ?)', bank, account_number, name) 223 | 224 | customer = repository(:sqlite3).get(BankCustomer, [bank, account_number]) 225 | customer.name.should == name 226 | 227 | repository(:sqlite3).destroy(customer).should be_true 228 | customer.should be_a_new_record 229 | customer.should be_dirty 230 | end 231 | 232 | it 'should respond to Resource#get' do 233 | bank, account_number, name = 'Conchords', '1100101', 'Robo Boogie' 234 | @adapter.execute('INSERT INTO "bank_customers" ("bank", "account_number", "name") VALUES (?, ?, ?)', bank, account_number, name) 235 | 236 | robots = repository(:sqlite3) { BankCustomer.get(bank, account_number) } 237 | 238 | robots.should_not be_nil 239 | robots.should_not be_dirty 240 | robots.should_not be_a_new_record 241 | robots.bank.should == bank 242 | robots.account_number.should == account_number 243 | end 244 | 245 | after do 246 | @adapter.execute('DROP TABLE "bank_customers"') 247 | end 248 | end 249 | end 250 | rescue LoadError => e 251 | describe 'do_sqlite3' do 252 | it 'should be required' do 253 | fail "SQLite3 integration specs not run! Could not load do_sqlite3: #{e}" 254 | end 255 | end 256 | end 257 | -------------------------------------------------------------------------------- /spec/integration/type_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | gem 'fastercsv', '>=1.2.3' 4 | require 'fastercsv' 5 | 6 | begin 7 | gem 'do_sqlite3', '=0.9.0' 8 | require 'do_sqlite3' 9 | 10 | DataMapper.setup(:sqlite3, "sqlite3://#{INTEGRATION_DB_PATH}") 11 | 12 | describe DataMapper::Type do 13 | 14 | before do 15 | 16 | @adapter = repository(:sqlite3).adapter 17 | @adapter.execute("CREATE TABLE coconuts (id INTEGER PRIMARY KEY, faked TEXT, document TEXT, stuff TEXT)") 18 | 19 | module TypeTests 20 | class Impostor < DataMapper::Type 21 | primitive String 22 | end 23 | 24 | class Coconut 25 | include DataMapper::Resource 26 | 27 | storage_names[:sqlite3] = 'coconuts' 28 | 29 | property :id, Fixnum, :serial => true 30 | property :faked, Impostor 31 | property :document, DM::Csv 32 | property :stuff, DM::Yaml 33 | end 34 | end 35 | 36 | @document = <<-EOS.margin 37 | NAME, RATING, CONVENIENCE 38 | Freebird's, 3, 3 39 | Whataburger, 1, 5 40 | Jimmy John's, 3, 4 41 | Mignon, 5, 2 42 | Fuzi Yao's, 5, 1 43 | Blue Goose, 5, 1 44 | EOS 45 | 46 | @stuff = YAML::dump({ 'Happy Cow!' => true, 'Sad Cow!' => false }) 47 | end 48 | 49 | it "should instantiate an object with custom types" do 50 | coconut = TypeTests::Coconut.new(:faked => 'bob', :document => @document, :stuff => @stuff) 51 | coconut.faked.should == 'bob' 52 | coconut.document.should be_a_kind_of(Array) 53 | coconut.stuff.should be_a_kind_of(Hash) 54 | end 55 | 56 | it "should CRUD an object with custom types" do 57 | repository(:sqlite3) do 58 | coconut = TypeTests::Coconut.new(:faked => 'bob', :document => @document, :stuff => @stuff) 59 | coconut.save.should be_true 60 | coconut.id.should_not be_nil 61 | 62 | fred = TypeTests::Coconut[coconut.id] 63 | fred.faked.should == 'bob' 64 | fred.document.should be_a_kind_of(Array) 65 | fred.stuff.should be_a_kind_of(Hash) 66 | 67 | texadelphia = ["Texadelphia", "5", "3"] 68 | 69 | # Figure out how to track these... possibly proxies? :-p 70 | document = fred.document.dup 71 | document << texadelphia 72 | fred.document = document 73 | 74 | stuff = fred.stuff.dup 75 | stuff['Manic Cow!'] = :maybe 76 | fred.stuff = stuff 77 | 78 | fred.save.should be_true 79 | 80 | # Can't call coconut.reload! since coconut.loaded_set isn't setup. 81 | mac = TypeTests::Coconut[fred.id] 82 | mac.document.last.should == texadelphia 83 | mac.stuff['Manic Cow!'].should == :maybe 84 | end 85 | end 86 | 87 | after do 88 | @adapter = repository(:sqlite3).adapter 89 | @adapter.execute("DROP TABLE coconuts") 90 | end 91 | end 92 | rescue LoadError 93 | warn "integration/type_spec not run! Could not load do_sqlite3." 94 | end 95 | -------------------------------------------------------------------------------- /spec/lib/mock_adapter.rb: -------------------------------------------------------------------------------- 1 | module DataMapper 2 | module Adapters 3 | class MockAdapter < DataMapper::Adapters::DataObjectsAdapter 4 | 5 | def create(repository, instance) 6 | instance 7 | end 8 | 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/spec.opts: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | gem 'rspec', '>=1.1.3' 3 | require 'pathname' 4 | require 'spec' 5 | require 'fileutils' 6 | 7 | require Pathname(__FILE__).dirname.expand_path.parent + 'lib/data_mapper' 8 | require DataMapper.root / 'spec' / 'lib' / 'mock_adapter' 9 | 10 | gem 'rspec', '>=1.1.3' 11 | 12 | INTEGRATION_DB_PATH = DataMapper.root / 'spec' / 'integration' / 'integration_test.db' 13 | 14 | FileUtils.touch INTEGRATION_DB_PATH unless INTEGRATION_DB_PATH.exist? 15 | 16 | DataMapper.setup(:default, 'mock://localhost') 17 | 18 | # Determine log path. 19 | ENV['_'] =~ /(\w+)/ 20 | log_path = DataMapper.root / 'log' / "#{$1 == 'opt' ? 'spec' : $1}.log" 21 | log_path.dirname.mkpath 22 | 23 | DataMapper::Logger.new(log_path, 0) 24 | at_exit { DataMapper.logger.close } 25 | 26 | class Article 27 | include DataMapper::Resource 28 | 29 | property :id, Fixnum, :serial => true 30 | property :blog_id, Fixnum 31 | property :created_at, DateTime 32 | property :author, String 33 | property :title, String 34 | 35 | class << self 36 | def property_by_name(name) 37 | properties(repository.name)[name] 38 | end 39 | end 40 | end 41 | 42 | class Comment 43 | include DataMapper::Resource 44 | end 45 | 46 | class NormalClass 47 | # should not include DataMapper::Resource 48 | end 49 | 50 | # ========================== 51 | # Used for Association specs 52 | class Vehicle 53 | include DataMapper::Resource 54 | 55 | property :id, Fixnum, :serial => true 56 | property :name, String 57 | end 58 | 59 | class Manufacturer 60 | include DataMapper::Resource 61 | 62 | property :id, Fixnum, :serial => true 63 | property :name, String 64 | end 65 | 66 | class Supplier 67 | include DataMapper::Resource 68 | 69 | property :id, Fixnum, :serial => true 70 | property :name, String 71 | end 72 | 73 | class Class 74 | def publicize_methods 75 | klass = class << self; self; end 76 | 77 | saved_private_class_methods = klass.private_instance_methods 78 | saved_protected_class_methods = klass.protected_instance_methods 79 | saved_private_instance_methods = self.private_instance_methods 80 | saved_protected_instance_methods = self.protected_instance_methods 81 | 82 | self.class_eval do 83 | klass.send(:public, *saved_private_class_methods) 84 | klass.send(:public, *saved_protected_class_methods) 85 | public(*saved_private_instance_methods) 86 | public(*saved_protected_instance_methods) 87 | end 88 | 89 | begin 90 | yield 91 | ensure 92 | self.class_eval do 93 | klass.send(:private, *saved_private_class_methods) 94 | klass.send(:protected, *saved_protected_class_methods) 95 | private(*saved_private_instance_methods) 96 | protected(*saved_protected_instance_methods) 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /spec/unit/adapters/adapter_shared_spec.rb: -------------------------------------------------------------------------------- 1 | 2 | describe "a DataMapper Adapter", :shared => true do 3 | 4 | it "should initialize the connection uri" do 5 | new_adapter = @adapter.class.new(:default, URI.parse('some://uri/string')) 6 | new_adapter.instance_variable_get('@uri').to_s.should == URI.parse('some://uri/string').to_s 7 | end 8 | 9 | %w{create read update delete read_one read_set delete_set} .each do |meth| 10 | it "should have a #{meth} method" do 11 | @adapter.should respond_to(meth.intern) 12 | end 13 | end 14 | 15 | end 16 | -------------------------------------------------------------------------------- /spec/unit/associations/many_to_many_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')) 2 | 3 | describe "DataMapper::Associations::ManyToMany" do 4 | 5 | before do 6 | @adapter = DataMapper::Repository.adapters[:relationship_spec] || DataMapper.setup(:relationship_spec, 'mock://localhost') 7 | end 8 | 9 | it "should allow a declaration" do 10 | 11 | lambda do 12 | class Supplier 13 | many_to_many :manufacturers 14 | end 15 | end.should_not raise_error 16 | end 17 | 18 | describe DataMapper::Associations::ManyToMany::Instance do 19 | before do 20 | @this = mock("this") 21 | @that = mock("that") 22 | @relationship = mock("relationship") 23 | @association = DataMapper::Associations::ManyToMany::Instance.new(@relationship, @that, nil) 24 | end 25 | 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /spec/unit/associations/many_to_one_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')) 2 | 3 | describe "DataMapper::Associations::ManyToOne" do 4 | 5 | it "should allow a declaration" do 6 | lambda do 7 | class Vehicle 8 | many_to_one :manufacturer 9 | end 10 | end.should_not raise_error 11 | end 12 | 13 | describe DataMapper::Associations::ManyToOne::Instance do 14 | before do 15 | @child = mock("child") 16 | @parent = mock("parent") 17 | @relationship = mock("relationship") 18 | @association = DataMapper::Associations::ManyToOne::Instance.new(@relationship, @child) 19 | end 20 | 21 | describe "when the parent exists" do 22 | it "should attach the parent to the child" do 23 | @parent.should_receive(:new_record?).and_return(false) 24 | @relationship.should_receive(:attach_parent).with(@child, @parent) 25 | 26 | @association.parent = @parent 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/unit/associations/one_to_many_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')) 2 | 3 | describe "DataMapper::Associations::OneToMany" do 4 | 5 | before do 6 | @class = Class.new do 7 | def self.name 8 | "Hannibal" 9 | end 10 | 11 | include DataMapper::Resource 12 | 13 | property :id, Fixnum 14 | 15 | send :one_to_many, :vehicles 16 | end 17 | 18 | @relationship = mock("relationship") 19 | end 20 | 21 | it "should install the association's methods" do 22 | victim = @class.new 23 | 24 | victim.should respond_to(:vehicles) 25 | end 26 | 27 | it "should work with classes inside modules" 28 | 29 | describe DataMapper::Associations::OneToMany::Proxy do 30 | describe "when loading" do 31 | def init 32 | DataMapper::Associations::OneToMany::Proxy.new(@relationship, nil) do |one, two| 33 | @tester.weee 34 | end 35 | end 36 | 37 | before do 38 | @tester = mock("tester") 39 | end 40 | 41 | it "should not load on initialize" do 42 | @tester.should_not_receive(:weee) 43 | init 44 | end 45 | 46 | it "should load when accessed" do 47 | @relationship.should_receive(:repository_name).and_return(:a_symbol) 48 | @tester.should_receive(:weee).and_return([]) 49 | a = init 50 | a.entries 51 | end 52 | end 53 | 54 | describe "when adding an element" do 55 | before do 56 | @parent = mock("parent") 57 | @element = mock("element", :null_object => true) 58 | @association = DataMapper::Associations::OneToMany::Proxy.new(@relationship, @parent) do 59 | [] 60 | end 61 | end 62 | 63 | describe "with a persisted parent" do 64 | it "should save the element" do 65 | @relationship.should_receive(:repository_name).and_return(:a_symbol) 66 | @parent.should_receive(:new_record?).and_return(false) 67 | @association.should_receive(:save_child).with(@element) 68 | 69 | @association << @element 70 | 71 | @association.instance_variable_get("@dirty_children").should be_empty 72 | end 73 | end 74 | 75 | describe "with a non-persisted parent" do 76 | it "should not save the element" do 77 | @relationship.should_receive(:repository_name).and_return(:a_symbol) 78 | @parent.should_receive(:new_record?).and_return(true) 79 | @association.should_not_receive(:save_child) 80 | 81 | @association << @element 82 | 83 | @association.instance_variable_get("@dirty_children").should_not be_empty 84 | end 85 | 86 | it "should save the element after the parent is saved" do 87 | 88 | end 89 | 90 | it "should add the parent's keys to the element after the parent is saved" 91 | end 92 | end 93 | 94 | describe "when deleting an element" do 95 | it "should delete the element from the database" do 96 | 97 | end 98 | 99 | it "should delete the element from the association" 100 | 101 | it "should erase the ex-parent's keys from the element" 102 | end 103 | 104 | describe "when deleting the parent" do 105 | 106 | end 107 | 108 | 109 | describe "with an unsaved parent" do 110 | describe "when deleting an element from an unsaved parent" do 111 | it "should remove the element from the association" do 112 | 113 | end 114 | end 115 | end 116 | end 117 | 118 | describe "when changing an element's parent" do 119 | 120 | end 121 | end 122 | -------------------------------------------------------------------------------- /spec/unit/associations/one_to_one_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')) 2 | 3 | describe "DataMapper::Associations::OneToOne" do 4 | 5 | it "should allow a declaration" do 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/unit/associations/relationship_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')) 2 | 3 | describe DataMapper::Associations::Relationship do 4 | 5 | before do 6 | @adapter = DataMapper::Repository.adapters[:relationship_spec] || DataMapper.setup(:relationship_spec, 'mock://localhost') 7 | end 8 | 9 | it "should describe an association" do 10 | belongs_to = DataMapper::Associations::Relationship.new( 11 | :manufacturer, 12 | {}, 13 | :relationship_spec, 14 | 'Vehicle', 15 | [ :manufacturer_id ], 16 | 'Manufacturer', 17 | nil 18 | ) 19 | 20 | belongs_to.should respond_to(:name) 21 | belongs_to.should respond_to(:repository_name) 22 | belongs_to.should respond_to(:child_key) 23 | belongs_to.should respond_to(:parent_key) 24 | end 25 | 26 | it "should map properties explicitly when an association method passes them in its options" do 27 | repository_name = :relationship_spec 28 | 29 | belongs_to = DataMapper::Associations::Relationship.new( 30 | :manufacturer, 31 | {}, 32 | repository_name, 33 | 'Vehicle', 34 | [ :manufacturer_id ], 35 | 'Manufacturer', 36 | [ :id ] 37 | ) 38 | 39 | belongs_to.name.should == :manufacturer 40 | belongs_to.repository_name.should == repository_name 41 | 42 | belongs_to.child_key.should be_a_kind_of(DataMapper::PropertySet) 43 | belongs_to.parent_key.should be_a_kind_of(DataMapper::PropertySet) 44 | 45 | belongs_to.child_key.to_a.should == Vehicle.properties(repository_name).slice(:manufacturer_id) 46 | belongs_to.parent_key.to_a.should == Manufacturer.properties(repository_name).key 47 | end 48 | 49 | it "should infer properties when options aren't passed" do 50 | repository_name = :relationship_spec 51 | 52 | has_many = DataMapper::Associations::Relationship.new( 53 | :models, 54 | {}, 55 | repository_name, 56 | 'Vehicle', 57 | nil, 58 | 'Manufacturer', 59 | nil 60 | ) 61 | 62 | has_many.name.should == :models 63 | has_many.repository_name.should == repository_name 64 | 65 | has_many.child_key.should be_a_kind_of(DataMapper::PropertySet) 66 | has_many.parent_key.should be_a_kind_of(DataMapper::PropertySet) 67 | 68 | has_many.child_key.to_a.should == Vehicle.properties(repository_name).slice(:models_id) 69 | has_many.parent_key.to_a.should == Manufacturer.properties(repository_name).key 70 | end 71 | 72 | it "should generate child properties with a safe subset of the parent options" do 73 | pending 74 | # For example, :size would be an option you'd want a generated child Property to copy, 75 | # but :serial or :key obviously not. So need to take a good look at Property::OPTIONS to 76 | # see what applies and what doesn't. 77 | end 78 | 79 | end 80 | 81 | __END__ 82 | class LazyLoadedSet < LoadedSet 83 | 84 | def initialize(*args, &b) 85 | super(*args) 86 | @on_demand_loader = b 87 | end 88 | 89 | def each 90 | @on_demand_loader[self] 91 | class << self 92 | def each 93 | super 94 | end 95 | end 96 | 97 | super 98 | end 99 | 100 | end 101 | 102 | set = LazyLoadedSet.new(repository, Zoo, { Property<:id> => 1, Property<:name> => 2, Property<:notes> => 3 }) do |lls| 103 | connection = create_connection 104 | command = connection.create_command("SELECT id, name, notes FROM zoos") 105 | command.set_types([Fixnum, String, String]) 106 | reader = command.execute_reader 107 | 108 | while(reader.next!) 109 | lls.load(reader.values) 110 | end 111 | 112 | reader.close 113 | connection.close 114 | end 115 | 116 | class AssociationSet 117 | 118 | def initialize(relationship) 119 | @relationship = relationship 120 | end 121 | 122 | def each 123 | # load some stuff 124 | end 125 | 126 | def <<(value) 127 | # add some stuff and track it. 128 | end 129 | end 130 | 131 | class Vehicle 132 | belongs_to :manufacturer 133 | 134 | def manufacturer 135 | manufacturer_association_set.first 136 | end 137 | 138 | def manufacturer=(value) 139 | manufacturer_association_set.set(value) 140 | end 141 | 142 | private 143 | # This is all class-evaled code defined by belongs_to: 144 | def manufacturer_association_set 145 | @manufacturer_association_set ||= AssociationSet.new( 146 | self.class.associations(repository.name)[:manufacturer] 147 | ) do |set| 148 | # This block is the part that will change between different associations. 149 | 150 | # Parent is the Array of PK properties remember. 151 | resource = set.relationship.parent.first.resource 152 | 153 | resource.all(resource.key => self.loaded_set.keys) 154 | end 155 | end 156 | 157 | end 158 | -------------------------------------------------------------------------------- /spec/unit/associations_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | describe "DataMapper::Associations" do 4 | before :each do 5 | @relationship = mock(DataMapper::Associations::Relationship) 6 | @n = 1.0/0 7 | end 8 | 9 | describe ".has" do 10 | 11 | it "should allow a declaration" do 12 | lambda do 13 | class Manufacturer 14 | has 1, :halo_car 15 | end 16 | end.should_not raise_error 17 | end 18 | 19 | it "should not allow a constraint that is not a Range, Fixnum, Bignum or Infinity" do 20 | lambda do 21 | class Manufacturer 22 | has '1', :halo_car 23 | end 24 | end.should raise_error(ArgumentError) 25 | end 26 | 27 | it "should not allow a constraint where the min is larger than the max" do 28 | lambda do 29 | class Manufacturer 30 | has 1..0, :halo_car 31 | end 32 | end.should raise_error(ArgumentError) 33 | end 34 | 35 | it "should not allow overwriting of the auto assigned min/max values with keys" do 36 | Manufacturer.should_receive(:one_to_many). 37 | with(:vehicles, {:min=>1, :max=>2}). 38 | and_return(@relationship) 39 | class Manufacturer 40 | has 1..2, :vehicles, :min=>5, :max=>10 41 | end 42 | end 43 | 44 | describe "one-to-one syntax" do 45 | it "should create a basic one-to-one association with fixed constraint" do 46 | Manufacturer.should_receive(:one_to_one). 47 | with(:halo_car, { :min => 1, :max => 1 }). 48 | and_return(@relationship) 49 | class Manufacturer 50 | has 1, :halo_car 51 | end 52 | end 53 | 54 | it "should create a basic one-to-one association with min/max constraints" do 55 | Manufacturer.should_receive(:one_to_one). 56 | with(:halo_car, { :min => 0, :max => 1 }). 57 | and_return(@relationship) 58 | class Manufacturer 59 | has 0..1, :halo_car 60 | end 61 | end 62 | 63 | it "should create a one-to-one association with options" do 64 | Manufacturer.should_receive(:one_to_one). 65 | with(:halo_car, {:class_name => 'Car', :min => 1, :max => 1 }). 66 | and_return(@relationship) 67 | class Manufacturer 68 | has 1, :halo_car, 69 | :class_name => 'Car' 70 | end 71 | end 72 | end 73 | 74 | describe "one-to-many syntax" do 75 | it "should create a basic one-to-many association with no constraints" do 76 | Manufacturer.should_receive(:one_to_many). 77 | with(:vehicles,{}). 78 | and_return(@relationship) 79 | class Manufacturer 80 | has n, :vehicles 81 | end 82 | end 83 | 84 | it "should create a one-to-many association with fixed constraint" do 85 | Manufacturer.should_receive(:one_to_many). 86 | with(:vehicles,{:min=>4, :max=>4}). 87 | and_return(@relationship) 88 | class Manufacturer 89 | has 4, :vehicles 90 | end 91 | end 92 | 93 | it "should create a one-to-many association with min/max constraints" do 94 | Manufacturer.should_receive(:one_to_many). 95 | with(:vehicles,{:min=>2, :max=>4}). 96 | and_return(@relationship) 97 | class Manufacturer 98 | has 2..4, :vehicles 99 | end 100 | end 101 | 102 | it "should create a one-to-many association with options" do 103 | Manufacturer.should_receive(:one_to_many). 104 | with(:vehicles,{:min=>1, :max=>@n, :class_name => 'Car'}). 105 | and_return(@relationship) 106 | class Manufacturer 107 | has 1..n, :vehicles, 108 | :class_name => 'Car' 109 | end 110 | end 111 | 112 | it "should create a many-to-many relationship if references are circular" do 113 | # ================ 114 | pending 115 | # ================ 116 | end 117 | 118 | it "should create one-to-many association and pass the :through option if specified" do 119 | Vehicle.should_receive(:one_to_many). 120 | with(:suppliers,{:through => :manufacturers}). 121 | and_return(@relationship) 122 | class Vehicle 123 | has n, :suppliers, :through => :manufacturers 124 | end 125 | end 126 | end 127 | end 128 | 129 | describe ".belongs_to" do 130 | it "should create a basic many-to-one association" do 131 | Manufacturer.should_receive(:many_to_one). 132 | with(:vehicle,{}). 133 | and_return(@relationship) 134 | class Manufacturer 135 | belongs_to :vehicle 136 | end 137 | end 138 | 139 | it "should create a many-to-one association with options" do 140 | Manufacturer.should_receive(:many_to_one). 141 | with(:vehicle,{:class_name => 'Car'}). 142 | and_return(@relationship) 143 | class Manufacturer 144 | belongs_to :vehicle, 145 | :class_name => 'Car' 146 | end 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /spec/unit/identity_map_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | describe "DataMapper::IdentityMap" do 4 | before(:all) do 5 | class Cow 6 | include DataMapper::Resource 7 | property :id, Fixnum, :key => true 8 | property :name, String 9 | end 10 | 11 | class Chicken 12 | include DataMapper::Resource 13 | property :name, String 14 | end 15 | 16 | class Pig 17 | include DataMapper::Resource 18 | property :id, Fixnum, :key => true 19 | property :composite, Fixnum, :key => true 20 | property :name, String 21 | end 22 | end 23 | 24 | it "should use a second level cache if created with on" 25 | 26 | it "should return nil on #get when it does not find the requested instance" do 27 | map = DataMapper::IdentityMap.new 28 | map.get([23]).should be_nil 29 | end 30 | 31 | it "should return an instance on #get when it finds the requested instance" do 32 | betsy = Cow.new({:id=>23,:name=>'Betsy'}) 33 | map = DataMapper::IdentityMap.new 34 | map.set(betsy.key, betsy) 35 | map.get([23]).should == betsy 36 | end 37 | 38 | it "should store an instance on #set" do 39 | betsy = Cow.new({:id=>23,:name=>'Betsy'}) 40 | map = DataMapper::IdentityMap.new 41 | map.set(betsy.key, betsy) 42 | map.get([23]).should == betsy 43 | end 44 | 45 | it "should store instances with composite keys on #set" do 46 | pig = Pig.new({:id=>1,:composite=>1,:name=> 'Pig'}) 47 | piggy = Pig.new({:id=>1,:composite=>2,:name=>'Piggy'}) 48 | 49 | map = DataMapper::IdentityMap.new 50 | map.set(pig.key, pig) 51 | map.set(piggy.key, piggy) 52 | 53 | map.get([1,1]).should == pig 54 | map.get([1,2]).should == piggy 55 | end 56 | 57 | it "should remove an instance on #delete" do 58 | betsy = Cow.new({:id=>23,:name=>'Betsy'}) 59 | map = DataMapper::IdentityMap.new 60 | map.set(betsy.key, betsy) 61 | map.delete([23]) 62 | map.get([23]).should be_nil 63 | end 64 | end 65 | 66 | describe "Second Level Caching" do 67 | 68 | before :all do 69 | @mock_class = Class.new do 70 | def get(key); raise NotImplementedError end 71 | def set(key, instance); raise NotImplementedError end 72 | def delete(key); raise NotImplementedError end 73 | end 74 | end 75 | 76 | it 'should expose a standard API' do 77 | cache = @mock_class.new 78 | cache.should respond_to(:get) 79 | cache.should respond_to(:set) 80 | cache.should respond_to(:delete) 81 | end 82 | 83 | it 'should provide values when the first level cache entry is empty' do 84 | cache = @mock_class.new 85 | key = %w[ test ] 86 | 87 | cache.should_receive(:get).with(key).once.and_return('resource') 88 | 89 | map = DataMapper::IdentityMap.new(cache) 90 | map.get(key).should == 'resource' 91 | end 92 | 93 | it 'should be set when the first level cache entry is set' do 94 | cache = @mock_class.new 95 | betsy = Cow.new(:id => 23, :name => 'Betsy') 96 | 97 | cache.should_receive(:set).with(betsy.key, betsy).once.and_return(betsy) 98 | 99 | map = DataMapper::IdentityMap.new(cache) 100 | map.set(betsy.key, betsy).should == betsy 101 | end 102 | 103 | it 'should be deleted when the first level cache entry is deleted' do 104 | cache = @mock_class.new 105 | betsy = Cow.new(:id => 23, :name => 'Betsy') 106 | 107 | cache.stub!(:set) 108 | cache.should_receive(:delete).with(betsy.key).once.and_return(betsy) 109 | 110 | map = DataMapper::IdentityMap.new(cache) 111 | map.set(betsy.key, betsy).should == betsy 112 | map.delete(betsy.key).should == betsy 113 | end 114 | 115 | it 'should not provide values when the first level cache entry is full' do 116 | cache = @mock_class.new 117 | betsy = Cow.new(:id => 23, :name => 'Betsy') 118 | 119 | cache.stub!(:set) 120 | cache.should_not_receive(:get) 121 | 122 | map = DataMapper::IdentityMap.new(cache) 123 | map.set(betsy.key, betsy).should == betsy 124 | map.get(betsy.key).should == betsy 125 | end 126 | end 127 | -------------------------------------------------------------------------------- /spec/unit/loaded_set_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | describe "DataMapper::LoadedSet" do 4 | 5 | before :all do 6 | DataMapper.setup(:default, "mock://localhost/mock") unless DataMapper::Repository.adapters[:default] 7 | DataMapper.setup(:other, "mock://localhost/mock") unless DataMapper::Repository.adapters[:other] 8 | 9 | @cow = Class.new do 10 | include DataMapper::Resource 11 | 12 | property :name, String, :key => true 13 | property :age, Fixnum 14 | end 15 | end 16 | 17 | it "should return the right repository" do 18 | klass = Class.new do 19 | include DataMapper::Resource 20 | end 21 | 22 | DataMapper::LoadedSet.new(repository(:other), klass, []).repository.name.should == :other 23 | end 24 | 25 | it "should be able to add arbitrary objects" do 26 | properties = @cow.properties(:default) 27 | properties_with_indexes = Hash[*properties.zip((0...properties.length).to_a).flatten] 28 | 29 | set = DataMapper::LoadedSet.new(DataMapper::repository(:default), @cow, properties_with_indexes) 30 | set.should respond_to(:reload) 31 | 32 | set.load(['Bob', 10]) 33 | set.load(['Nancy', 11]) 34 | 35 | results = set.entries 36 | results.should have(2).entries 37 | 38 | results.each do |cow| 39 | cow.instance_variables.should include('@name') 40 | cow.instance_variables.should include('@age') 41 | end 42 | 43 | bob, nancy = results[0], results[1] 44 | 45 | bob.name.should eql('Bob') 46 | bob.age.should eql(10) 47 | bob.should_not be_a_new_record 48 | 49 | nancy.name.should eql('Nancy') 50 | nancy.age.should eql(11) 51 | nancy.should_not be_a_new_record 52 | 53 | results.first.should == bob 54 | end 55 | 56 | end 57 | 58 | describe "DataMapper::LazyLoadedSet" do 59 | 60 | before :all do 61 | DataMapper.setup(:default, "mock://localhost/mock") unless DataMapper::Repository.adapters[:default] 62 | 63 | @cow = Class.new do 64 | include DataMapper::Resource 65 | 66 | property :name, String, :key => true 67 | property :age, Fixnum 68 | end 69 | 70 | properties = @cow.properties(:default) 71 | @properties_with_indexes = Hash[*properties.zip((0...properties.length).to_a).flatten] 72 | end 73 | 74 | it "should raise an error if no block is provided" do 75 | lambda { set = DataMapper::LazyLoadedSet.new(DataMapper::repository(:default), @cow, @properties_with_indexes) }.should raise_error 76 | end 77 | 78 | it "should make a materialization block" do 79 | set = DataMapper::LazyLoadedSet.new(DataMapper::repository(:default), @cow, @properties_with_indexes) do |lls| 80 | lls.load(['Bob', 10]) 81 | lls.load(['Nancy', 11]) 82 | end 83 | 84 | results = set.entries 85 | results.size.should == 2 86 | end 87 | 88 | it "should be eachable" do 89 | set = DataMapper::LazyLoadedSet.new(DataMapper::repository(:default), @cow, @properties_with_indexes) do |lls| 90 | lls.load(['Bob', 10]) 91 | lls.load(['Nancy', 11]) 92 | end 93 | 94 | set.each do |x| 95 | x.name.should be_a_kind_of(String) 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /spec/unit/naming_conventions_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | describe "DataMapper::NamingConventions" do 4 | it "should coerce a string into the Underscored convention" do 5 | DataMapper::NamingConventions::Underscored.call('User').should == 'user' 6 | DataMapper::NamingConventions::Underscored.call('UserAccountSetting').should == 'user_account_setting' 7 | end 8 | 9 | it "should coerce a string into the UnderscoredAndPluralized convention" do 10 | DataMapper::NamingConventions::UnderscoredAndPluralized.call('User').should == 'users' 11 | DataMapper::NamingConventions::UnderscoredAndPluralized.call('UserAccountSetting').should == 'user_account_settings' 12 | end 13 | 14 | it "should coerce a string into the UnderscoredAndPluralized convention joining namespace with underscore" do 15 | DataMapper::NamingConventions::UnderscoredAndPluralized.call('Model::User').should == 'model_users' 16 | DataMapper::NamingConventions::UnderscoredAndPluralized.call('Model::UserAccountSetting').should == 'model_user_account_settings' 17 | end 18 | 19 | it "should coerce a string into the UnderscoredAndPluralizedWithoutModule convention" do 20 | DataMapper::NamingConventions::UnderscoredAndPluralizedWithoutModule.call('Model::User').should == 'users' 21 | DataMapper::NamingConventions::UnderscoredAndPluralizedWithoutModule.call('Model::UserAccountSetting').should == 'user_account_settings' 22 | end 23 | 24 | it "should coerce a string into the Yaml convention" do 25 | DataMapper::NamingConventions::Yaml.call('UserSetting').should == 'user_settings.yaml' 26 | DataMapper::NamingConventions::Yaml.call('User').should == 'users.yaml' 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/unit/property_set_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | class Icon 4 | include DataMapper::Resource 5 | 6 | property :id, Fixnum, :serial => true 7 | property :name, String 8 | property :width, Fixnum, :lazy => true 9 | property :height, Fixnum, :lazy => true 10 | end 11 | 12 | class Boat 13 | include DataMapper::Resource 14 | property :name, String #not lazy 15 | property :text, DataMapper::Types::Text #Lazy by default 16 | property :notes, String, :lazy => true 17 | property :a1, String, :lazy => [:ctx_a,:ctx_c] 18 | property :a2, String, :lazy => [:ctx_a,:ctx_b] 19 | property :a3, String, :lazy => [:ctx_a] 20 | property :b1, String, :lazy => [:ctx_b] 21 | property :b2, String, :lazy => [:ctx_b] 22 | property :b3, String, :lazy => [:ctx_b] 23 | end 24 | 25 | describe DataMapper::PropertySet do 26 | before :each do 27 | @properties = Icon.properties(:default) 28 | end 29 | 30 | it "#slice should find properties" do 31 | @properties.slice(:name, 'width').should have(2).entries 32 | end 33 | 34 | it "#select should find properties" do 35 | @properties.select { |property| property.type == Fixnum }.should have(3).entries 36 | end 37 | 38 | it "#[] should find properties by name (Symbol or String)" do 39 | default_properties = [ :id, 'name', :width, 'height' ] 40 | @properties.each_with_index do |property,i| 41 | property.should == @properties[default_properties[i]] 42 | end 43 | end 44 | 45 | it "should provide defaults" do 46 | @properties.defaults.should have(2).entries 47 | @properties.should have(4).entries 48 | end 49 | 50 | it 'should add a property for lazy loading to the :default context if a context is not supplied' do 51 | Boat.properties(:default).lazy_context(:default).length.should == 2 # text & notes 52 | end 53 | 54 | it 'should return a list of contexts that a given field is in' do 55 | props = Boat.properties(:default) 56 | set = props.property_contexts(:a1) 57 | set.include?(:ctx_a).should == true 58 | set.include?(:ctx_c).should == true 59 | set.include?(:ctx_b).should == false 60 | end 61 | 62 | it 'should return a list of expanded fields that should be loaded with a given field' do 63 | props = Boat.properties(:default) 64 | set = props.lazy_load_context(:a2) 65 | expect = [:a1,:a2,:a3,:b1,:b2,:b3] 66 | expect.should == set.sort! {|a,b| a.to_s <=> b.to_s} 67 | end 68 | 69 | describe 'when dup\'ed' do 70 | it 'should duplicate the @entries ivar' do 71 | @properties.dup.entries.should_not equal(@properties.entries) 72 | end 73 | 74 | it 'should reinitialize @properties_for' do 75 | # force @properties_for to hold a property 76 | Icon.properties(:default)[:name].should_not be_nil 77 | @properties = Icon.properties(:default) 78 | 79 | @properties.instance_variable_get("@property_for").should_not be_empty 80 | @properties.dup.instance_variable_get("@property_for").should be_empty 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /spec/unit/property_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | describe DataMapper::Property do 4 | 5 | before(:all) do 6 | class Zoo 7 | include DataMapper::Resource 8 | end 9 | 10 | class Name < DataMapper::Type 11 | primitive String 12 | size 100 13 | end 14 | 15 | class Tomato 16 | include DataMapper::Resource 17 | end 18 | end 19 | 20 | it "should evaluate two similar properties as equal" do 21 | p1 = DataMapper::Property.new(Zoo, :name, String, { :size => 30 }) 22 | p2 = DataMapper::Property.new(Zoo, :name, String, { :size => 30 }) 23 | p3 = DataMapper::Property.new(Zoo, :title, String, { :size => 30 }) 24 | p1.eql?(p2).should == true 25 | p1.hash.should == p2.hash 26 | p1.eql?(p3).should == false 27 | p1.hash.should_not == p3.hash 28 | end 29 | 30 | it "should create a String property" do 31 | property = DataMapper::Property.new(Zoo, :name, String, { :size => 30 }) 32 | 33 | property.primitive.should == String 34 | end 35 | 36 | it "should use a custom type Name property" do 37 | class Name < DataMapper::Type 38 | primitive String 39 | end 40 | 41 | property = DataMapper::Property.new(Zoo, :name, Name, {}) 42 | 43 | property.primitive.should == String 44 | property.type.should == Name 45 | property.primitive.should == property.type.primitive 46 | end 47 | 48 | it "should override type options with property options" do 49 | property = DataMapper::Property.new(Zoo, :name, Name, { :size => 50 }) 50 | options = property.instance_variable_get(:@options) 51 | 52 | options[:size].should == 50 53 | end 54 | 55 | it "should determine nullness" do 56 | DataMapper::Property.new(Tomato,:botanical_name,String,{:nullable => true}).options[:nullable].should == true 57 | end 58 | 59 | it "should determine its name" do 60 | DataMapper::Property.new(Tomato,:botanical_name,String,{}).name.should == :botanical_name 61 | end 62 | 63 | it "should determine lazyness" do 64 | DataMapper::Property.new(Tomato,:botanical_name,String,{:lazy => true}).lazy?.should == true 65 | DataMapper::Property.new(Tomato,:seedless,TrueClass,{}).lazy?.should == false 66 | end 67 | 68 | it "should automatically set lazyness to true on text fields?" do 69 | DataMapper::Property.new(Tomato,:botanical_name,DataMapper::Types::Text,{}).lazy?.should == true 70 | end 71 | 72 | it "should determine keyness" do 73 | DataMapper::Property.new(Tomato,:id,Fixnum,{:key => true}).key?.should == true 74 | DataMapper::Property.new(Tomato,:botanical_name,String,{}).key?.should == false 75 | end 76 | 77 | it "should determine serialness" do 78 | DataMapper::Property.new(Tomato,:id,Fixnum,{:serial => true}).serial?.should == true 79 | DataMapper::Property.new(Tomato,:botanical_name,String,{}).serial?.should == false 80 | end 81 | 82 | it "should determine lockability" do 83 | DataMapper::Property.new(Tomato, :id, Fixnum, { :lock => true }).lock?.should == true 84 | DataMapper::Property.new(Tomato, :botanical_name, String, {}).lock?.should == false 85 | end 86 | 87 | # TODO should we add an accessor method property.default_value 88 | it "should determine a default value" do 89 | DataMapper::Property.new(Tomato,:botanical_name,String,{:default => 'Tomato'}).options[:default].should == 'Tomato' 90 | end 91 | 92 | it "should determine visibility of readers and writers" do 93 | name = DataMapper::Property.new(Tomato,:botanical_name,String,{}) 94 | name.reader_visibility.should == :public 95 | name.writer_visibility.should == :public 96 | 97 | seeds = DataMapper::Property.new(Tomato,:seeds,TrueClass,{:accessor=>:private}) 98 | seeds.reader_visibility.should == :private 99 | seeds.writer_visibility.should == :private 100 | 101 | family = DataMapper::Property.new(Tomato,:family,String,{:reader => :public, :writer => :private }) 102 | family.reader_visibility.should == :public 103 | family.writer_visibility.should == :private 104 | end 105 | 106 | it "should return an instance variable name" do 107 | DataMapper::Property.new(Tomato,:flavor,String,{}).instance_variable_name.should == '@flavor' 108 | DataMapper::Property.new(Tomato,:ripe,TrueClass,{}).instance_variable_name.should == '@ripe' #not @ripe? 109 | end 110 | 111 | it "should append ? to TrueClass property reader methods" do 112 | class Potato 113 | include DataMapper::Resource 114 | property :fresh, TrueClass 115 | end 116 | Potato.new().should respond_to(:fresh?) 117 | Potato.new(:fresh => true).should be_fresh 118 | end 119 | 120 | it "should raise an ArgumentError when created with an invalid option" do 121 | lambda{ 122 | DataMapper::Property.new(Tomato,:botanical_name,String,{:foo=>:bar}) 123 | }.should raise_error(ArgumentError) 124 | end 125 | 126 | it 'should return the attribute value from a given instance' do 127 | class Tomahto 128 | include DataMapper::Resource 129 | property :id, Fixnum, :key => true 130 | end 131 | 132 | tomato = Tomahto.new(:id => 1) 133 | tomato.class.properties(:default)[:id].get(tomato).should == 1 134 | end 135 | 136 | it 'should set the attribute value in a given instance' do 137 | tomato = Tomahto.new 138 | tomato.class.properties(:default)[:id].set(tomato, 2) 139 | tomato.id.should == 2 140 | end 141 | 142 | it 'should respond to custom?' do 143 | DataMapper::Property.new(Zoo, :name, Name, { :size => 50 }).should be_custom 144 | DataMapper::Property.new(Zoo, :state, String, { :size => 2 }).should_not be_custom 145 | end 146 | 147 | it "should set the field to the correct field_naming_convention" do 148 | DataMapper::Property.new(Zoo, :species, String, {}).field.should == 'species' 149 | DataMapper::Property.new(Tomato, :genetic_history, DataMapper::Types::Text, {}).field.should == "genetic_history" 150 | end 151 | 152 | it "should provide the primitive mapping" do 153 | DataMapper::Property.new(Zoo, :poverty, String, {}).primitive.should == String 154 | DataMapper::Property.new(Zoo, :fortune, DataMapper::Types::Text, {}).primitive.should == String 155 | end 156 | 157 | it 'should provide typecast' do 158 | DataMapper::Property.new(Zoo, :name, String).should respond_to(:typecast) 159 | end 160 | 161 | it 'should pass through the value if it is the same type when typecasting' do 162 | value = 'San Diego' 163 | property = DataMapper::Property.new(Zoo, :name, String) 164 | property.typecast(value).object_id.should == value.object_id 165 | end 166 | 167 | it 'should pass through the value nil when typecasting' do 168 | property = DataMapper::Property.new(Zoo, :string, String) 169 | property.typecast(nil).should == nil 170 | end 171 | 172 | it 'should pass through the value for an Object property' do 173 | value = 'a ruby object' 174 | property = DataMapper::Property.new(Zoo, :object, Object) 175 | property.typecast(value).object_id.should == value.object_id 176 | end 177 | 178 | it 'should typecast value (true) for a TrueClass property' do 179 | property = DataMapper::Property.new(Zoo, :true_class, TrueClass) 180 | property.typecast(true).should == true 181 | end 182 | 183 | it 'should typecast value ("true") for a TrueClass property' do 184 | property = DataMapper::Property.new(Zoo, :true_class, TrueClass) 185 | property.typecast('true').should == true 186 | end 187 | 188 | it 'should typecast value for a String property' do 189 | property = DataMapper::Property.new(Zoo, :string, String) 190 | property.typecast(0).should == '0' 191 | end 192 | 193 | it 'should typecast value for a Float property' do 194 | property = DataMapper::Property.new(Zoo, :float, Float) 195 | property.typecast('0.0').should == 0.0 196 | end 197 | 198 | it 'should typecast value for a Fixnum property' do 199 | property = DataMapper::Property.new(Zoo, :fixnum, Fixnum) 200 | property.typecast('0').should == 0 201 | end 202 | 203 | it 'should typecast value for a BigDecimal property' do 204 | property = DataMapper::Property.new(Zoo, :big_decimal, BigDecimal) 205 | property.typecast(0.0).should == BigDecimal.new('0.0') 206 | end 207 | 208 | it 'should typecast value for a DateTime property' do 209 | property = DataMapper::Property.new(Zoo, :date_time, DateTime) 210 | property.typecast('2000-01-01 00:00:00').should == DateTime.new(2000, 1, 1, 0, 0, 0) 211 | end 212 | 213 | it 'should typecast value for a Date property' do 214 | property = DataMapper::Property.new(Zoo, :date, Date) 215 | property.typecast('2000-01-01').should == Date.new(2000, 1, 1) 216 | end 217 | 218 | it 'should typecast value for a Class property' do 219 | property = DataMapper::Property.new(Zoo, :class, Class) 220 | property.typecast('Zoo').should == Zoo 221 | end 222 | 223 | it 'should provide inspect' do 224 | DataMapper::Property.new(Zoo, :name, String).should respond_to(:inspect) 225 | end 226 | 227 | it 'should return an abbreviated representation of the property when inspected' do 228 | DataMapper::Property.new(Zoo, :name, String).inspect.should == '#' 229 | end 230 | 231 | it 'should raise a SyntaxError when the name contains invalid characters' do 232 | lambda { DataMapper::Property.new(Zoo, :"with space", TrueClass) }.should raise_error(SyntaxError) 233 | end 234 | end 235 | -------------------------------------------------------------------------------- /spec/unit/repository_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | describe DataMapper::Repository do 4 | 5 | before do 6 | @adapter = DataMapper::Repository.adapters[:repository_spec] || DataMapper.setup(:repository_spec, 'mock://localhost') 7 | 8 | class Vegetable 9 | include DataMapper::Resource 10 | 11 | property :id, Fixnum, :serial => true 12 | property :name, String 13 | 14 | end 15 | end 16 | 17 | describe "managing transactions" do 18 | it "should create a new Transaction with itself as argument when #transaction is called" do 19 | trans = mock("transaction") 20 | repo = repository 21 | DataMapper::Adapters::Transaction.should_receive(:new).once.with(repo).and_return(trans) 22 | repo.transaction.should == trans 23 | end 24 | end 25 | 26 | it "should provide persistance methods" do 27 | repository.should respond_to(:get) 28 | repository.should respond_to(:first) 29 | repository.should respond_to(:all) 30 | repository.should respond_to(:save) 31 | repository.should respond_to(:destroy) 32 | end 33 | 34 | it 'should call #create when #save is called on a new record' do 35 | repository = repository(:repository_spec) 36 | instance = Vegetable.new({:id => 1, :name => 'Potato'}) 37 | 38 | @adapter.should_receive(:create).with(repository, instance).and_return(instance) 39 | 40 | repository.save(instance) 41 | end 42 | 43 | it 'should call #update when #save is called on an existing record' do 44 | repository = repository(:repository_spec) 45 | instance = Vegetable.new(:name => 'Potato') 46 | instance.instance_variable_set('@new_record', false) 47 | 48 | @adapter.should_receive(:update).with(repository, instance).and_return(instance) 49 | 50 | repository.save(instance) 51 | end 52 | 53 | it 'should provide default_name' do 54 | DataMapper::Repository.should respond_to(:default_name) 55 | end 56 | 57 | it 'should return :default for default_name' do 58 | DataMapper::Repository.default_name.should == :default 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/unit/resource_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | # rSpec completely FUBARs everything if you give it a Module here. 4 | # So we give it a String of the module name instead. 5 | # DO NOT CHANGE THIS! 6 | describe "DataMapper::Resource" do 7 | 8 | before :all do 9 | 10 | DataMapper.setup(:default, "mock://localhost/mock") unless DataMapper::Repository.adapters[:default] 11 | DataMapper.setup(:legacy, "mock://localhost/mock") unless DataMapper::Repository.adapters[:legacy] 12 | 13 | unless DataMapper::Repository.adapters[:yet_another_repository] 14 | adapter = DataMapper.setup(:yet_another_repository, "mock://localhost/mock") 15 | adapter.resource_naming_convention = DataMapper::NamingConventions::Underscored 16 | end 17 | 18 | class Planet 19 | 20 | include DataMapper::Resource 21 | 22 | storage_names[:legacy] = "dying_planets" 23 | 24 | property :id, Fixnum, :key => true 25 | property :name, String, :lock => true 26 | property :age, Fixnum 27 | property :core, String, :private => true 28 | property :type, Class 29 | 30 | repository(:legacy) do 31 | property :cowabunga, String 32 | end 33 | end 34 | 35 | class Moon 36 | end 37 | 38 | class LegacyStar 39 | include DataMapper::Resource 40 | def self.default_repository_name 41 | :legacy 42 | end 43 | end 44 | end 45 | 46 | it "should hold repository-specific properties" do 47 | Planet.properties(:legacy).should have_property(:cowabunga) 48 | Planet.properties.should_not have_property(:cowabunga) 49 | end 50 | 51 | it "should track the classes that include it" do 52 | DataMapper::Resource.including_classes.clear 53 | Moon.class_eval do include(DataMapper::Resource) end 54 | DataMapper::Resource.including_classes.should == Set.new([Moon]) 55 | end 56 | 57 | it "should return an instance of the created object" do 58 | Planet.create(:name => 'Venus', :age => 1_000_000, :core => nil, :id => 42).should be_a_kind_of(Planet) 59 | end 60 | 61 | it 'should provide persistance methods' do 62 | planet = Planet.new 63 | planet.should respond_to(:new_record?) 64 | planet.should respond_to(:save) 65 | planet.should respond_to(:destroy) 66 | end 67 | 68 | it "should have attributes" do 69 | attributes = { :name => 'Jupiter', :age => 1_000_000, :core => nil, :id => 42, :type => nil } 70 | jupiter = Planet.new(attributes) 71 | jupiter.attributes.should == attributes 72 | end 73 | 74 | it "should be able to set attributes (including private attributes)" do 75 | attributes = { :name => 'Jupiter', :age => 1_000_000, :core => nil, :id => 42, :type => nil } 76 | jupiter = Planet.new(attributes) 77 | jupiter.attributes.should == attributes 78 | jupiter.attributes = attributes.merge({ :core => 'Magma' }) 79 | jupiter.attributes.should == attributes 80 | jupiter.send(:private_attributes=, attributes.merge({ :core => 'Magma' })) 81 | jupiter.attributes.should == attributes.merge({ :core => 'Magma' }) 82 | end 83 | 84 | it "should track attributes" do 85 | 86 | # So attribute tracking is a feature of the Resource, 87 | # not the Property. Properties are class-level declarations. 88 | # Instance-level operations like this happen in Resource with methods 89 | # and ivars it sets up. Like a @dirty_attributes Array for example to 90 | # track dirty attributes. 91 | 92 | mars = Planet.new :name => 'Mars' 93 | # #attribute_loaded? and #attribute_dirty? are a bit verbose, 94 | # but I like the consistency and grouping of the methods. 95 | 96 | # initialize-set values are dirty as well. DM sets ivars 97 | # directly when materializing, so an ivar won't exist 98 | # if the value wasn't loaded by DM initially. Touching that 99 | # ivar at all will declare it, so at that point it's loaded. 100 | # This means #attribute_loaded?'s implementation could be very 101 | # similar (if not identical) to: 102 | # def attribute_loaded?(name) 103 | # instance_variable_defined?("@#{name}") 104 | # end 105 | mars.attribute_loaded?(:name).should be_true 106 | mars.attribute_dirty?(:name).should be_true 107 | mars.attribute_loaded?(:age).should be_false 108 | 109 | mars.age.should be_nil 110 | 111 | # So accessing a value should ensure it's loaded. 112 | # XXX: why? if the @ivar isn't set, which it wouldn't be in this 113 | # case because mars is a new_record?, then perhaps it should return 114 | # false 115 | # mars.attribute_loaded?(:age).should be_true 116 | 117 | # A value should be able to be both loaded and nil. 118 | mars[:age].should be_nil 119 | 120 | # Unless you call #[]= it's not dirty. 121 | mars.attribute_dirty?(:age).should be_false 122 | 123 | mars[:age] = 30 124 | # Obviously. :-) 125 | mars.attribute_dirty?(:age).should be_true 126 | 127 | mars.should respond_to(:shadow_attribute_get) 128 | end 129 | 130 | it 'should return the dirty attributes' do 131 | pluto = Planet.new(:name => 'Pluto', :age => 500_000) 132 | pluto.attribute_dirty?(:name).should be_true 133 | pluto.attribute_dirty?(:age).should be_true 134 | end 135 | 136 | it 'should overwite old dirty attributes with new ones' do 137 | pluto = Planet.new(:name => 'Pluto', :age => 500_000) 138 | pluto.dirty_attributes.size.should == 2 139 | pluto.attribute_dirty?(:name).should be_true 140 | pluto.attribute_dirty?(:age).should be_true 141 | pluto.name = "pluto" 142 | pluto.dirty_attributes.size.should == 2 143 | pluto.attribute_dirty?(:name).should be_true 144 | pluto.attribute_dirty?(:age).should be_true 145 | end 146 | 147 | it 'should provide a key' do 148 | Planet.new.should respond_to(:key) 149 | end 150 | 151 | it 'should temporarily store original values for locked attributes' do 152 | mars = Planet.new 153 | mars.instance_variable_set('@name', 'Mars') 154 | mars.instance_variable_set('@new_record', false) 155 | 156 | mars[:name] = 'God of War' 157 | mars[:name].should == 'God of War' 158 | mars.name.should == 'God of War' 159 | mars.shadow_attribute_get(:name).should == 'Mars' 160 | end 161 | 162 | describe 'ClassMethods' do 163 | 164 | it "should return a new Transaction with itself as argument on #transaction" do 165 | transaction = mock("transaction") 166 | DataMapper::Adapters::Transaction.should_receive(:new).once.with(Planet).and_return(transaction) 167 | Planet.transaction.should == transaction 168 | end 169 | 170 | it 'should add hook functionality to including class' do 171 | Planet.should respond_to(:before) 172 | Planet.should respond_to(:after) 173 | end 174 | 175 | it 'should provide a repository' do 176 | Planet.should respond_to(:repository) 177 | end 178 | 179 | it '.repository should delegate to DataMapper.repository' do 180 | repository = mock('repository') 181 | DataMapper.should_receive(:repository).with(:legacy).once.and_return(repository) 182 | Planet.repository(:legacy).should == repository 183 | end 184 | 185 | it '.repository should use default repository when not passed any arguments' do 186 | Planet.repository.name.should == Planet.repository(:default).name 187 | LegacyStar.repository.name.should == LegacyStar.repository(:legacy).name 188 | end 189 | 190 | it 'should provide storage_name' do 191 | Planet.should respond_to(:storage_name) 192 | end 193 | 194 | it '.storage_name should map a repository to the storage location' do 195 | Planet.storage_name(:legacy).should == 'dying_planets' 196 | end 197 | 198 | it '.storage_name should use default repository when not passed any arguments' do 199 | Planet.storage_name.object_id.should == Planet.storage_name(:default).object_id 200 | end 201 | 202 | it 'should provide storage_names' do 203 | Planet.should respond_to(:storage_names) 204 | end 205 | 206 | it '.storage_names should return a Hash mapping each repository to a storage location' do 207 | Planet.storage_names.should be_kind_of(Hash) 208 | Planet.storage_names.should == { :default => 'planets', :legacy => 'dying_planets' } 209 | end 210 | 211 | it 'should provide property' do 212 | Planet.should respond_to(:property) 213 | end 214 | 215 | it 'should specify property' 216 | 217 | it 'should provide properties' do 218 | Planet.should respond_to(:properties) 219 | end 220 | 221 | it '.properties should return an PropertySet' do 222 | Planet.properties(:legacy).should be_kind_of(DataMapper::PropertySet) 223 | Planet.properties(:legacy).should have(6).entries 224 | end 225 | 226 | it '.properties should use default repository when not passed any arguments' do 227 | Planet.properties.object_id.should == Planet.properties(:default).object_id 228 | end 229 | 230 | it 'should provide key' do 231 | Planet.should respond_to(:key) 232 | end 233 | 234 | it '.key should return an Array of Property objects' do 235 | Planet.key(:legacy).should be_kind_of(Array) 236 | Planet.key(:legacy).should have(1).entries 237 | Planet.key(:legacy).first.should be_kind_of(DataMapper::Property) 238 | end 239 | 240 | it '.key should use default repository when not passed any arguments' do 241 | Planet.key.object_id.should == Planet.key(:default).object_id 242 | end 243 | 244 | it 'should provide inheritance_property' do 245 | Planet.should respond_to(:inheritance_property) 246 | end 247 | 248 | it '.inheritance_property should return a Property object' do 249 | Planet.inheritance_property(:legacy).should be_kind_of(DataMapper::Property) 250 | Planet.inheritance_property(:legacy).name.should == :type 251 | Planet.inheritance_property(:legacy).type.should == Class 252 | end 253 | 254 | it '.inheritance_property should use default repository when not passed any arguments' do 255 | Planet.inheritance_property.object_id.should == Planet.inheritance_property(:default).object_id 256 | end 257 | 258 | it 'should provide finder methods' do 259 | Planet.should respond_to(:get) 260 | Planet.should respond_to(:first) 261 | Planet.should respond_to(:all) 262 | Planet.should respond_to(:[]) 263 | end 264 | end 265 | 266 | describe 'when retrieving by key' do 267 | it 'should return the corresponding object' do 268 | m = mock("planet") 269 | Planet.should_receive(:get).with(1).and_return(m) 270 | 271 | Planet[1].should == m 272 | end 273 | 274 | it 'should raise an error if not found' do 275 | Planet.should_receive(:get).and_return(nil) 276 | 277 | lambda do 278 | Planet[1] 279 | end.should raise_error(DataMapper::ObjectNotFoundError) 280 | end 281 | end 282 | end 283 | -------------------------------------------------------------------------------- /spec/unit/scope_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | DataMapper.setup(:mock, "mock:///mock.db") 4 | 5 | describe DataMapper::Scope do 6 | after do 7 | Article.publicize_methods do 8 | Article.scope_stack.clear # reset the stack before each spec 9 | end 10 | end 11 | 12 | describe '.with_scope' do 13 | it 'should be protected' do 14 | klass = class << Article; self; end 15 | klass.should be_protected_method_defined(:with_scope) 16 | end 17 | 18 | it 'should set the current scope for the block when given a Hash' do 19 | Article.publicize_methods do 20 | Article.with_scope :blog_id => 1 do 21 | Article.current_scope.should == DataMapper::Query.new(repository(:mock), Article, :blog_id => 1) 22 | end 23 | end 24 | end 25 | 26 | it 'should set the current scope for the block when given a DataMapper::Query' do 27 | Article.publicize_methods do 28 | Article.with_scope query = DataMapper::Query.new(repository(:mock), Article) do 29 | Article.current_scope.should == query 30 | end 31 | end 32 | end 33 | 34 | it 'should set the current scope for an inner block, merged with the outer scope' do 35 | Article.publicize_methods do 36 | Article.with_scope :blog_id => 1 do 37 | Article.with_scope :author => 'dkubb' do 38 | Article.current_scope.should == DataMapper::Query.new(repository(:mock), Article, :blog_id => 1, :author => 'dkubb') 39 | end 40 | end 41 | end 42 | end 43 | 44 | it 'should reset the stack on error' do 45 | Article.publicize_methods do 46 | Article.current_scope.should be_nil 47 | lambda { 48 | Article.with_scope(:blog_id => 1) { raise 'There was a problem!' } 49 | }.should raise_error(RuntimeError) 50 | Article.current_scope.should be_nil 51 | end 52 | end 53 | end 54 | 55 | describe '.with_exclusive_scope' do 56 | it 'should be protected' do 57 | klass = class << Article; self; end 58 | klass.should be_protected_method_defined(:with_exclusive_scope) 59 | end 60 | 61 | it 'should set the current scope for an inner block, ignoring the outer scope' do 62 | Article.publicize_methods do 63 | Article.with_scope :blog_id => 1 do 64 | Article.with_exclusive_scope :author => 'dkubb' do 65 | Article.current_scope.should == DataMapper::Query.new(repository(:mock), Article, :author => 'dkubb') 66 | end 67 | end 68 | end 69 | end 70 | 71 | it 'should reset the stack on error' do 72 | Article.publicize_methods do 73 | Article.current_scope.should be_nil 74 | lambda { 75 | Article.with_exclusive_scope(:blog_id => 1) { raise 'There was a problem!' } 76 | }.should raise_error(RuntimeError) 77 | Article.current_scope.should be_nil 78 | end 79 | end 80 | end 81 | 82 | describe '.scope_stack' do 83 | it 'should be private' do 84 | klass = class << Article; self; end 85 | klass.should be_private_method_defined(:scope_stack) 86 | end 87 | 88 | it 'should provide an Array' do 89 | Article.publicize_methods do 90 | Article.scope_stack.should be_kind_of(Array) 91 | end 92 | end 93 | 94 | it 'should be the same in a thread' do 95 | Article.publicize_methods do 96 | Article.scope_stack.object_id.should == Article.scope_stack.object_id 97 | end 98 | end 99 | 100 | it 'should be different in each thread' do 101 | Article.publicize_methods do 102 | a = Thread.new { Article.scope_stack } 103 | b = Thread.new { Article.scope_stack } 104 | 105 | a.value.object_id.should_not == b.value.object_id 106 | end 107 | end 108 | end 109 | 110 | describe '.current_scope' do 111 | it 'should be private' do 112 | klass = class << Article; self; end 113 | klass.should be_private_method_defined(:current_scope) 114 | end 115 | 116 | it 'should return nil if the scope stack is empty' do 117 | Article.publicize_methods do 118 | Article.scope_stack.should be_empty 119 | Article.current_scope.should be_nil 120 | end 121 | end 122 | 123 | it 'should return the last element of the scope stack' do 124 | Article.publicize_methods do 125 | query = DataMapper::Query.new(repository(:mock), Article) 126 | Article.scope_stack << query 127 | Article.current_scope.object_id.should == query.object_id 128 | end 129 | end 130 | end 131 | 132 | # TODO: specify the behavior of finders (all, first, get, []) when scope is in effect 133 | end 134 | -------------------------------------------------------------------------------- /spec/unit/support/blank_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')) 2 | 3 | describe Object do 4 | it 'should provide blank?' do 5 | Object.new.should respond_to(:blank?) 6 | end 7 | 8 | it 'should be blank if it is nil' do 9 | object = Object.new 10 | class << object 11 | def nil?; true end 12 | end 13 | object.should be_blank 14 | end 15 | 16 | it 'should be blank if it is empty' do 17 | {}.should be_blank 18 | [].should be_blank 19 | end 20 | 21 | it 'should not be blank if not nil or empty' do 22 | Object.new.should_not be_blank 23 | [nil].should_not be_blank 24 | { nil => 0 }.should_not be_blank 25 | end 26 | end 27 | 28 | describe Numeric do 29 | it 'should provide blank?' do 30 | 1.should respond_to(:blank?) 31 | end 32 | 33 | it 'should never be blank' do 34 | 1.should_not be_blank 35 | end 36 | end 37 | 38 | describe NilClass do 39 | it 'should provide blank?' do 40 | nil.should respond_to(:blank?) 41 | end 42 | 43 | it 'should always be blank' do 44 | nil.should be_blank 45 | end 46 | end 47 | 48 | describe TrueClass do 49 | it 'should provide blank?' do 50 | true.should respond_to(:blank?) 51 | end 52 | 53 | it 'should never be blank' do 54 | true.should_not be_blank 55 | end 56 | end 57 | 58 | describe FalseClass do 59 | it 'should provide blank?' do 60 | false.should respond_to(:blank?) 61 | end 62 | 63 | it 'should always be blank' do 64 | false.should be_blank 65 | end 66 | end 67 | 68 | describe String do 69 | it 'should provide blank?' do 70 | 'string'.should respond_to(:blank?) 71 | end 72 | 73 | it 'should be blank if empty' do 74 | ''.should be_blank 75 | end 76 | 77 | it 'should be blank if it only contains whitespace' do 78 | ' '.should be_blank 79 | " \r \n \t ".should be_blank 80 | end 81 | 82 | it 'should not be blank if it contains non-whitespace' do 83 | ' a '.should_not be_blank 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /spec/unit/support/inflection_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')) 2 | 3 | describe DataMapper::Inflection do 4 | 5 | it 'should pluralize a word' do 6 | 'car'.plural.should == 'cars' 7 | DataMapper::Inflection.pluralize('car').should == 'cars' 8 | end 9 | 10 | it 'should singularize a word' do 11 | "cars".singular.should == "car" 12 | DataMapper::Inflection.singularize('cars').should == 'car' 13 | end 14 | 15 | it 'should classify an underscored name' do 16 | DataMapper::Inflection.classify('data_mapper').should == 'DataMapper' 17 | end 18 | 19 | it 'should camelize an underscored name' do 20 | DataMapper::Inflection.camelize('data_mapper').should == 'DataMapper' 21 | end 22 | 23 | it 'should underscore a camelized name' do 24 | DataMapper::Inflection.underscore('DataMapper').should == 'data_mapper' 25 | end 26 | 27 | it 'should humanize names' do 28 | DataMapper::Inflection.humanize('employee_salary').should == 'Employee salary' 29 | DataMapper::Inflection.humanize('author_id').should == 'Author' 30 | end 31 | 32 | it 'should demodulize a module name' do 33 | DataMapper::Inflection.demodulize('DataMapper::Inflector').should == 'Inflector' 34 | end 35 | 36 | it 'should tableize a name (underscore with last word plural)' do 37 | DataMapper::Inflection.tableize('fancy_category').should == 'fancy_categories' 38 | DataMapper::Inflection.tableize('FancyCategory').should == 'fancy_categories' 39 | end 40 | 41 | it 'should create a fk name from a class name' do 42 | DataMapper::Inflection.foreign_key('Message').should == 'message_id' 43 | DataMapper::Inflection.foreign_key('Admin::Post').should == 'post_id' 44 | end 45 | 46 | 47 | 48 | 49 | 50 | end 51 | -------------------------------------------------------------------------------- /spec/unit/support/object_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')) 2 | 3 | describe Object do 4 | 5 | it "should be able to get a recursive constant" do 6 | Object.recursive_const_get('DataMapper::Resource').should == DataMapper::Resource 7 | end 8 | 9 | it "should not cache unresolvable class string" do 10 | lambda { Object.recursive_const_get('Foo::Bar::Baz') }.should raise_error(NameError) 11 | Object.instance_variable_get(:@nested_constants).has_key?('Foo::Bar::Baz').should == false 12 | end 13 | 14 | end 15 | -------------------------------------------------------------------------------- /spec/unit/support/string_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')) 2 | 3 | describe String do 4 | it 'should translate' do 5 | '%s is great!'.t('DataMapper').should == 'DataMapper is great!' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/unit/support/struct_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')) 2 | 3 | describe Struct do 4 | 5 | it "should have attributes" do 6 | 7 | s = Struct.new(:name).new('bob') 8 | s.attributes.should == { :name => 'bob' } 9 | 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /spec/unit/type_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper')) 2 | 3 | describe DataMapper::Type do 4 | 5 | before(:each) do 6 | class TestType < DataMapper::Type 7 | primitive String 8 | size 10 9 | end 10 | 11 | class TestType2 < DataMapper::Type 12 | primitive String 13 | size 10 14 | 15 | def self.load(value, property) 16 | value.reverse 17 | end 18 | 19 | def self.dump(value, property) 20 | value.reverse 21 | end 22 | end 23 | 24 | class TestResource 25 | include DataMapper::Resource 26 | end 27 | 28 | class TestType3 < DataMapper::Type 29 | primitive String 30 | size 10 31 | attr_accessor :property, :value 32 | 33 | def self.load(value, property) 34 | type = self.new 35 | type.property = property 36 | type.value = value 37 | type 38 | end 39 | 40 | def self.dump(value, property) 41 | value.value 42 | end 43 | end 44 | end 45 | 46 | it "should have the same PROPERTY_OPTIONS aray as DataMapper::Property" do 47 | # pending("currently there is no way to read PROPERTY_OPTIONS and aliases from DataMapper::Property. Also, some properties need to be defined as aliases instead of being listed in the PROPERTY_OPTIONS array") 48 | DataMapper::Type::PROPERTY_OPTIONS.should == DataMapper::Property::PROPERTY_OPTIONS 49 | end 50 | 51 | it "should create a new type based on String primitive" do 52 | TestType.primitive.should == String 53 | end 54 | 55 | it "should have size of 10" do 56 | TestType.size.should == 10 57 | end 58 | 59 | it "should have options hash exactly equal to options specified in custom type" do 60 | #ie. it should not include null elements 61 | TestType.options.should == { :size => 10, :length => 10 } 62 | end 63 | 64 | it "should have length aliased to size" do 65 | TestType.length.should == TestType.size 66 | end 67 | 68 | it "should pass through the value if load wasn't overriden" do 69 | TestType.load("test", nil).should == "test" 70 | end 71 | 72 | it "should pass through the value if dump wasn't overriden" do 73 | TestType.dump("test", nil).should == "test" 74 | end 75 | 76 | it "should not raise NotImplmenetedException if load was overriden" do 77 | TestType2.dump("helo", nil).should == "oleh" 78 | end 79 | 80 | it "should not raise NotImplmenetedException if dump was overriden" do 81 | TestType2.load("oleh", nil).should == "helo" 82 | end 83 | 84 | describe "using a custom type" do 85 | before do 86 | @property = DataMapper::Property.new TestResource, :name, TestType3, {} 87 | end 88 | 89 | it "should return a object of the same type" do 90 | TestType3.load("helo", @property).class.should == TestType3 91 | end 92 | 93 | it "should contain the property" do 94 | TestType3.load("helo", @property).property.should == @property 95 | end 96 | 97 | it "should contain the value" do 98 | TestType3.load("helo", @property).value.should == "helo" 99 | end 100 | 101 | it "should return the value" do 102 | obj = TestType3.load("helo", @property) 103 | TestType3.dump(obj, @property).should == "helo" 104 | end 105 | end 106 | 107 | describe "using def Type" do 108 | before do 109 | @class = Class.new(DataMapper::Type(String, :size => 20)) 110 | end 111 | 112 | it "should be of the specified type" do 113 | @class.primitive.should == String 114 | end 115 | 116 | it "should have the right options set" do 117 | @class.size.should == 20 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /spec/unit/types/enum_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')) 2 | 3 | include DataMapper::Types 4 | 5 | describe DataMapper::Types::Enum do 6 | 7 | describe ".new" do 8 | it "should create a Class" do 9 | Enum.new.should be_instance_of(Class) 10 | end 11 | 12 | it "should create unique a Class each call" do 13 | Enum.new.should_not == Enum.new 14 | end 15 | 16 | it "should use the arguments as the values in the @flag_map hash" do 17 | Enum.new(:first, :second, :third).flag_map.values.should == [:first, :second, :third] 18 | end 19 | 20 | it "should create incremental keys for the @flag_map hash, staring at 1" do 21 | Enum.new(:one, :two, :three, :four).flag_map.keys.should == (1..4).to_a 22 | end 23 | end 24 | 25 | describe ".[]" do 26 | it "should be an alias for the new method" do 27 | Enum.should_receive(:new).with(:uno, :dos, :tres) 28 | Enum[:uno, :dos, :tres] 29 | end 30 | end 31 | 32 | describe ".dump" do 33 | before(:each) do 34 | @enum = Enum[:first, :second, :third] 35 | end 36 | 37 | it "should return the key of the value match from the flag map" do 38 | @enum.dump(:first).should == 1 39 | @enum.dump(:second).should == 2 40 | @enum.dump(:third).should == 3 41 | end 42 | 43 | it "should return nil if there is no match" do 44 | @enum.dump(:zero).should be_nil 45 | end 46 | end 47 | 48 | describe ".load" do 49 | before(:each) do 50 | @enum = Enum[:uno, :dos, :tres] 51 | end 52 | 53 | it "should return the value of the key match from the flag map" do 54 | @enum.load(1).should == :uno 55 | @enum.load(2).should == :dos 56 | @enum.load(3).should == :tres 57 | end 58 | 59 | it "should return nil if there is no key" do 60 | @enum.load(-1).should be_nil 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/unit/types/flag_spec.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'spec_helper')) 2 | 3 | include DataMapper::Types 4 | 5 | describe DataMapper::Types::Flag do 6 | 7 | describe ".new" do 8 | it "should create a Class" do 9 | Flag.new.should be_instance_of(Class) 10 | end 11 | 12 | it "should create unique a Class each call" do 13 | Flag.new.should_not == Flag.new 14 | end 15 | 16 | it "should use the arguments as the values in the @flag_map hash" do 17 | Flag.new(:first, :second, :third).flag_map.values.should == [:first, :second, :third] 18 | end 19 | 20 | it "should create keys by the 2 power series for the @flag_map hash, staring at 1" do 21 | Flag.new(:one, :two, :three, :four, :five).flag_map.keys.should include(1, 2, 4, 8, 16) 22 | end 23 | end 24 | 25 | describe ".[]" do 26 | it "should be an alias for the new method" do 27 | Flag.should_receive(:new).with(:uno, :dos, :tres) 28 | Flag[:uno, :dos, :tres] 29 | end 30 | end 31 | 32 | describe ".dump" do 33 | before(:each) do 34 | @flag = Flag[:first, :second, :third, :fourth, :fifth] 35 | end 36 | 37 | it "should return the key of the value match from the flag map" do 38 | @flag.dump(:first).should == 1 39 | @flag.dump(:second).should == 2 40 | @flag.dump(:third).should == 4 41 | @flag.dump(:fourth).should == 8 42 | @flag.dump(:fifth).should == 16 43 | end 44 | 45 | it "should return a binary flag built from the key values of all matches" do 46 | @flag.dump(:first, :second).should == 3 47 | @flag.dump(:second, :fourth).should == 10 48 | @flag.dump(:first, :second, :third, :fourth, :fifth).should == 31 49 | end 50 | 51 | it "should return 0 if there is no match" do 52 | @flag.dump(:zero).should == 0 53 | end 54 | end 55 | 56 | describe ".load" do 57 | before(:each) do 58 | @flag = Flag[:uno, :dos, :tres, :cuatro, :cinco] 59 | end 60 | 61 | it "should return the value of the key match from the flag map" do 62 | @flag.load(1).should == [:uno] 63 | @flag.load(2).should == [:dos] 64 | @flag.load(4).should == [:tres] 65 | @flag.load(8).should == [:cuatro] 66 | @flag.load(16).should == [:cinco] 67 | end 68 | 69 | it "should return an array of all flags matches" do 70 | @flag.load(3).should include(:uno, :dos) 71 | @flag.load(10).should include(:dos, :cuatro) 72 | @flag.load(31).should include(:uno, :dos, :tres, :cuatro, :cinco) 73 | end 74 | 75 | it "should return an empty array if there is no key" do 76 | @flag.load(-1).should == [] 77 | @flag.load(nil).should == [] 78 | @flag.load(32).should == [] 79 | end 80 | end 81 | end 82 | --------------------------------------------------------------------------------