├── .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