├── .gitignore ├── LICENSE ├── Manifest ├── README ├── Rakefile ├── ambition.gemspec ├── app_generators └── ambition_adapter │ ├── USAGE │ ├── ambition_adapter_generator.rb │ └── templates │ ├── LICENSE │ ├── README │ ├── Rakefile │ ├── lib │ ├── adapter │ │ ├── base.rb.erb │ │ ├── query.rb.erb │ │ ├── select.rb.erb │ │ ├── slice.rb.erb │ │ └── sort.rb.erb │ └── init.rb.erb │ └── test │ ├── helper.rb.erb │ ├── select_test.rb.erb │ ├── slice_test.rb.erb │ └── sort_test.rb.erb ├── bin └── ambition_adapter ├── deps.rip ├── lib ├── ambition.rb └── ambition │ ├── api.rb │ ├── context.rb │ ├── core_ext.rb │ ├── enumerable.rb │ ├── processors │ ├── base.rb │ ├── ruby.rb │ ├── select.rb │ ├── slice.rb │ └── sort.rb │ └── sexp_translator.rb ├── site ├── Rakefile └── src │ ├── _adapters.textile │ ├── adapters.textile │ ├── adapters │ └── activerecord.textile │ ├── api.textile │ ├── index.textile │ ├── layout.textile │ └── static │ ├── bg.png │ ├── code_highlighter.js │ ├── css.js │ ├── html.js │ ├── hubris.css │ ├── javascript.js │ └── ruby.js └── test ├── adapters └── exemplar │ ├── association_test.rb │ ├── count_test.rb │ ├── detect_test.rb │ ├── enumerable_test.rb │ ├── helper.rb │ ├── index_operator.rb │ ├── reject_test.rb │ ├── select_test.rb │ ├── slice_test.rb │ └── sort_test.rb ├── debug └── helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | live 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007 Chris Wanstrath 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /Manifest: -------------------------------------------------------------------------------- 1 | app_generators/ambition_adapter/ambition_adapter_generator.rb 2 | app_generators/ambition_adapter/templates/lib/adapter/base.rb.erb 3 | app_generators/ambition_adapter/templates/lib/adapter/query.rb.erb 4 | app_generators/ambition_adapter/templates/lib/adapter/select.rb.erb 5 | app_generators/ambition_adapter/templates/lib/adapter/slice.rb.erb 6 | app_generators/ambition_adapter/templates/lib/adapter/sort.rb.erb 7 | app_generators/ambition_adapter/templates/lib/init.rb.erb 8 | app_generators/ambition_adapter/templates/LICENSE 9 | app_generators/ambition_adapter/templates/Rakefile 10 | app_generators/ambition_adapter/templates/README 11 | app_generators/ambition_adapter/templates/test/helper.rb.erb 12 | app_generators/ambition_adapter/templates/test/select_test.rb.erb 13 | app_generators/ambition_adapter/templates/test/slice_test.rb.erb 14 | app_generators/ambition_adapter/templates/test/sort_test.rb.erb 15 | app_generators/ambition_adapter/USAGE 16 | bin/ambition_adapter 17 | lib/ambition/api.rb 18 | lib/ambition/context.rb 19 | lib/ambition/core_ext.rb 20 | lib/ambition/enumerable.rb 21 | lib/ambition/processors/base.rb 22 | lib/ambition/processors/ruby.rb 23 | lib/ambition/processors/select.rb 24 | lib/ambition/processors/slice.rb 25 | lib/ambition/processors/sort.rb 26 | lib/ambition/sexp_translator.rb 27 | lib/ambition.rb 28 | LICENSE 29 | Manifest 30 | README 31 | test/adapters/exemplar/association_test.rb 32 | test/adapters/exemplar/count_test.rb 33 | test/adapters/exemplar/detect_test.rb 34 | test/adapters/exemplar/enumerable_test.rb 35 | test/adapters/exemplar/helper.rb 36 | test/adapters/exemplar/index_operator.rb 37 | test/adapters/exemplar/reject_test.rb 38 | test/adapters/exemplar/select_test.rb 39 | test/adapters/exemplar/slice_test.rb 40 | test/adapters/exemplar/sort_test.rb 41 | test/debug 42 | test/helper.rb 43 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | = Ambition 2 | 3 | 4 | == Get it 5 | 6 | 7 | 8 | $ git clone git://github.com/defunkt/ambition.git 9 | 10 | == Resources 11 | 12 | * http://ambition.rubyforge.org/ 13 | * http://groups.google.com/group/ambition-rb/ 14 | * http://errtheblog.com/posts/63-full-of-ambition 15 | * http://errtheblog.com/posts/82-adapting-ambitiously 16 | * http://errtheblog.com/posts/86-sugary-adapters 17 | * http://errtheblog.com/posts/64-even-more-ambitious 18 | 19 | 20 | 21 | == Author 22 | 23 | Chris Wanstrath 24 | chris@ozmm.org 25 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'rake/rdoctask' 4 | 5 | Version = '0.5.4' 6 | 7 | module Rake::TaskManager 8 | def delete_task(task_class, *args, &block) 9 | task_name, deps = resolve_args(args) 10 | @tasks.delete(task_class.scope_name(@scope, task_name).to_s) 11 | end 12 | end 13 | class Rake::Task 14 | def self.delete_task(args, &block) Rake.application.delete_task(self, args, &block) end 15 | end 16 | def delete_task(args, &block) Rake::Task.delete_task(args, &block) end 17 | 18 | begin 19 | require 'rubygems' 20 | gem 'echoe', '>=2.7' 21 | ENV['RUBY_FLAGS'] = "" 22 | require 'echoe' 23 | 24 | Echoe.new('ambition', Version) do |p| 25 | p.project = 'err' 26 | p.summary = "Ambition builds yer API calls from plain jane Ruby." 27 | p.description = "Ambition builds yer API calls from plain jane Ruby." 28 | p.url = "http://errtheblog.com/" 29 | p.author = 'Chris Wanstrath' 30 | p.email = "chris@ozmm.org" 31 | p.ruby_version = '>= 1.8.6' 32 | p.ignore_pattern = /^(\.git|site|adapters).+/ 33 | p.test_pattern = 'test/*_test.rb' 34 | p.dependencies << 'ParseTree =2.1.1' 35 | p.dependencies << 'ruby2ruby =1.1.8' 36 | p.dependencies << 'rubigen =1.1.1' 37 | end 38 | 39 | rescue LoadError 40 | puts "Not doing any of the Echoe gemmy stuff, because you don't have the specified gem versions" 41 | end 42 | 43 | delete_task :test 44 | delete_task :install_gem 45 | 46 | Rake::TestTask.new('test') do |t| 47 | t.pattern = 'test/*_test.rb' 48 | end 49 | 50 | Rake::TestTask.new('test:adapters') do |t| 51 | t.pattern = 'adapters/*/test/*_test.rb' 52 | end 53 | 54 | Dir['adapters/*'].each do |adapter| 55 | adapter = adapter.split('/').last 56 | Rake::TestTask.new("test:adapters:#{adapter.sub('ambitious_','')}") do |t| 57 | t.pattern = "adapters/#{adapter}/test/*_test.rb" 58 | end 59 | end 60 | 61 | desc 'Default: run unit tests.' 62 | task :default => :test 63 | 64 | desc 'Generate RDoc documentation' 65 | Rake::RDocTask.new(:rdoc) do |rdoc| 66 | files = ['README', 'LICENSE', 'lib/**/*.rb'] 67 | rdoc.rdoc_files.add(files) 68 | rdoc.main = "README" 69 | rdoc.title = "ambition" 70 | # rdoc.template = File.exists?(t="/Users/chris/ruby/projects/err/rock/template.rb") ? t : "/var/www/rock/template.rb" 71 | rdoc.rdoc_dir = 'doc' 72 | rdoc.options << '--inline-source' 73 | end 74 | 75 | desc 'Generate coverage reports' 76 | task :rcov do 77 | `rcov -e gems test/*_test.rb` 78 | puts 'Generated coverage reports.' 79 | end 80 | 81 | desc 'Install as a gem' 82 | task :install_gem do 83 | puts `rake manifest package && gem install pkg/ambition-#{Version}.gem` 84 | end 85 | -------------------------------------------------------------------------------- /ambition.gemspec: -------------------------------------------------------------------------------- 1 | 2 | # Gem::Specification for Ambition-0.5.4 3 | # Originally generated by Echoe 4 | 5 | Gem::Specification.new do |s| 6 | s.name = %q{ambition} 7 | s.version = "0.5.4" 8 | 9 | s.specification_version = 2 if s.respond_to? :specification_version= 10 | 11 | s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= 12 | s.authors = ["Chris Wanstrath"] 13 | s.date = %q{2008-04-26} 14 | s.default_executable = %q{ambition_adapter} 15 | s.description = %q{Ambition builds yer API calls from plain jane Ruby.} 16 | s.email = %q{chris@ozmm.org} 17 | s.executables = ["ambition_adapter"] 18 | s.extra_rdoc_files = ["bin/ambition_adapter", "lib/ambition/api.rb", "lib/ambition/context.rb", "lib/ambition/core_ext.rb", "lib/ambition/enumerable.rb", "lib/ambition/processors/base.rb", "lib/ambition/processors/ruby.rb", "lib/ambition/processors/select.rb", "lib/ambition/processors/slice.rb", "lib/ambition/processors/sort.rb", "lib/ambition/sexp_translator.rb", "lib/ambition.rb", "LICENSE", "README"] 19 | s.files = ["app_generators/ambition_adapter/ambition_adapter_generator.rb", "app_generators/ambition_adapter/templates/lib/adapter/base.rb.erb", "app_generators/ambition_adapter/templates/lib/adapter/query.rb.erb", "app_generators/ambition_adapter/templates/lib/adapter/select.rb.erb", "app_generators/ambition_adapter/templates/lib/adapter/slice.rb.erb", "app_generators/ambition_adapter/templates/lib/adapter/sort.rb.erb", "app_generators/ambition_adapter/templates/lib/init.rb.erb", "app_generators/ambition_adapter/templates/LICENSE", "app_generators/ambition_adapter/templates/Rakefile", "app_generators/ambition_adapter/templates/README", "app_generators/ambition_adapter/templates/test/helper.rb.erb", "app_generators/ambition_adapter/templates/test/select_test.rb.erb", "app_generators/ambition_adapter/templates/test/slice_test.rb.erb", "app_generators/ambition_adapter/templates/test/sort_test.rb.erb", "app_generators/ambition_adapter/USAGE", "bin/ambition_adapter", "lib/ambition/api.rb", "lib/ambition/context.rb", "lib/ambition/core_ext.rb", "lib/ambition/enumerable.rb", "lib/ambition/processors/base.rb", "lib/ambition/processors/ruby.rb", "lib/ambition/processors/select.rb", "lib/ambition/processors/slice.rb", "lib/ambition/processors/sort.rb", "lib/ambition/sexp_translator.rb", "lib/ambition.rb", "LICENSE", "Manifest", "README", "test/adapters/exemplar/association_test.rb", "test/adapters/exemplar/count_test.rb", "test/adapters/exemplar/detect_test.rb", "test/adapters/exemplar/enumerable_test.rb", "test/adapters/exemplar/helper.rb", "test/adapters/exemplar/index_operator.rb", "test/adapters/exemplar/reject_test.rb", "test/adapters/exemplar/select_test.rb", "test/adapters/exemplar/slice_test.rb", "test/adapters/exemplar/sort_test.rb", "test/debug", "test/helper.rb", "ambition.gemspec"] 20 | s.has_rdoc = true 21 | s.homepage = %q{http://errtheblog.com/} 22 | s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Ambition", "--main", "README"] 23 | s.require_paths = ["lib"] 24 | s.required_ruby_version = Gem::Requirement.new(">= 1.8.6") 25 | s.rubyforge_project = %q{err} 26 | s.rubygems_version = %q{1.0.1} 27 | s.summary = %q{Ambition builds yer API calls from plain jane Ruby.} 28 | 29 | s.add_dependency(%q, ["= 2.1.1"]) 30 | s.add_dependency(%q, ["= 1.1.8"]) 31 | s.add_dependency(%q, ["= 1.1.1"]) 32 | end 33 | 34 | 35 | # # Original Rakefile source (requires the Echoe gem): 36 | # 37 | # require 'rake' 38 | # require 'rake/testtask' 39 | # require 'rake/rdoctask' 40 | # 41 | # Version = '0.5.4' 42 | # 43 | # module Rake::TaskManager 44 | # def delete_task(task_class, *args, &block) 45 | # task_name, deps = resolve_args(args) 46 | # @tasks.delete(task_class.scope_name(@scope, task_name).to_s) 47 | # end 48 | # end 49 | # class Rake::Task 50 | # def self.delete_task(args, &block) Rake.application.delete_task(self, args, &block) end 51 | # end 52 | # def delete_task(args, &block) Rake::Task.delete_task(args, &block) end 53 | # 54 | # begin 55 | # require 'rubygems' 56 | # gem 'echoe', '>=2.7' 57 | # ENV['RUBY_FLAGS'] = "" 58 | # require 'echoe' 59 | # 60 | # Echoe.new('ambition', Version) do |p| 61 | # p.project = 'err' 62 | # p.summary = "Ambition builds yer API calls from plain jane Ruby." 63 | # p.description = "Ambition builds yer API calls from plain jane Ruby." 64 | # p.url = "http://errtheblog.com/" 65 | # p.author = 'Chris Wanstrath' 66 | # p.email = "chris@ozmm.org" 67 | # p.ruby_version = '>= 1.8.6' 68 | # p.ignore_pattern = /^(\.git|site|adapters).+/ 69 | # p.test_pattern = 'test/*_test.rb' 70 | # p.dependencies << 'ParseTree =2.1.1' 71 | # p.dependencies << 'ruby2ruby =1.1.8' 72 | # p.dependencies << 'rubigen =1.1.1' 73 | # end 74 | # 75 | # rescue LoadError 76 | # puts "Not doing any of the Echoe gemmy stuff, because you don't have the specified gem versions" 77 | # end 78 | # 79 | # delete_task :test 80 | # delete_task :install_gem 81 | # 82 | # Rake::TestTask.new('test') do |t| 83 | # t.pattern = 'test/*_test.rb' 84 | # end 85 | # 86 | # Rake::TestTask.new('test:adapters') do |t| 87 | # t.pattern = 'adapters/*/test/*_test.rb' 88 | # end 89 | # 90 | # Dir['adapters/*'].each do |adapter| 91 | # adapter = adapter.split('/').last 92 | # Rake::TestTask.new("test:adapters:#{adapter.sub('ambitious_','')}") do |t| 93 | # t.pattern = "adapters/#{adapter}/test/*_test.rb" 94 | # end 95 | # end 96 | # 97 | # desc 'Default: run unit tests.' 98 | # task :default => :test 99 | # 100 | # desc 'Generate RDoc documentation' 101 | # Rake::RDocTask.new(:rdoc) do |rdoc| 102 | # files = ['README', 'LICENSE', 'lib/**/*.rb'] 103 | # rdoc.rdoc_files.add(files) 104 | # rdoc.main = "README" 105 | # rdoc.title = "ambition" 106 | # # rdoc.template = File.exists?(t="/Users/chris/ruby/projects/err/rock/template.rb") ? t : "/var/www/rock/template.rb" 107 | # rdoc.rdoc_dir = 'doc' 108 | # rdoc.options << '--inline-source' 109 | # end 110 | # 111 | # desc 'Generate coverage reports' 112 | # task :rcov do 113 | # `rcov -e gems test/*_test.rb` 114 | # puts 'Generated coverage reports.' 115 | # end 116 | # 117 | # desc 'Install as a gem' 118 | # task :install_gem do 119 | # puts `rake manifest package && gem install pkg/ambition-#{Version}.gem` 120 | # end 121 | -------------------------------------------------------------------------------- /app_generators/ambition_adapter/USAGE: -------------------------------------------------------------------------------- 1 | o. 2 | -------------------------------------------------------------------------------- /app_generators/ambition_adapter/ambition_adapter_generator.rb: -------------------------------------------------------------------------------- 1 | class AmbitionAdapterGenerator < RubiGen::Base 2 | # DEFAULT_SHEBANG = File.join(Config::CONFIG['bindir'], Config::CONFIG['ruby_install_name']) 3 | # default_options :author => nil 4 | 5 | attr_reader :adapter_name, :adapter_module 6 | 7 | def initialize(runtime_args, runtime_options = {}) 8 | super 9 | usage if args.empty? 10 | @destination_root = File.expand_path(args.shift) 11 | base_name = self.base_name.sub(/ambitious(-|_)/,'') 12 | @adapter_name = base_name 13 | @adapter_module = base_name.split('_').map { |part| part[0] = part[0...1].upcase; part }.join 14 | extract_options 15 | end 16 | 17 | def manifest 18 | record do |m| 19 | # Ensure appropriate folder(s) exists 20 | m.directory '' 21 | dirs = %W( 22 | lib/ambition/adapters/#{adapter_name} 23 | test 24 | ) 25 | dirs.each { |path| m.directory path } 26 | 27 | ## 28 | # Translator / Query stubs 29 | adapter_path = "lib/ambition/adapters/#{adapter_name}" 30 | 31 | %w( base query select slice sort ).each do |file| 32 | m.template "lib/adapter/#{file}.rb.erb", "#{adapter_path}/#{file}.rb" 33 | end 34 | 35 | m.template 'lib/init.rb.erb', "#{adapter_path}.rb" 36 | 37 | ## 38 | # Test stubs 39 | Dir[File.dirname(__FILE__) + '/templates/test/*.rb.erb'].each do |file| 40 | file = File.basename(file, '.*') 41 | m.template "test/#{file}.erb", "test/#{file}" 42 | end 43 | 44 | ## 45 | # Normal files 46 | files = %w( LICENSE README Rakefile ) 47 | files.each do |file| 48 | m.template file, file 49 | end 50 | end 51 | end 52 | 53 | protected 54 | def banner 55 | "Usage: ambition_adapter adapter_name" 56 | end 57 | 58 | def add_options!(opts) 59 | opts.separator '' 60 | opts.separator 'Options:' 61 | opts.on("-v", "--version", "Show the #{File.basename($0)} version number and quit.") 62 | opts.separator '' 63 | end 64 | 65 | def extract_options; end 66 | end 67 | -------------------------------------------------------------------------------- /app_generators/ambition_adapter/templates/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2007 Your Name 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /app_generators/ambition_adapter/templates/README: -------------------------------------------------------------------------------- 1 | <%= string = "An Ambitious #{adapter_module} Adapter" %> 2 | <%= '=' * string.size %> 3 | 4 | More information on Ambition: 5 | -> http://ambition.rubyforge.org 6 | -> http://groups.google.com/group/ambition-rb/ 7 | -------------------------------------------------------------------------------- /app_generators/ambition_adapter/templates/Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | 3 | Version = '0.1.0' 4 | 5 | begin 6 | require 'rubygems' 7 | gem 'echoe', '>=2.7' 8 | ENV['RUBY_FLAGS'] = "" 9 | require 'echoe' 10 | 11 | Echoe.new('ambitious-<%= adapter_name %>') do |p| 12 | p.dependencies << '<%= adapter_name %> >=1.0' 13 | p.summary = "An ambitious adapter for <%= adapter_module %>" 14 | p.author = 'Your Name' 15 | p.email = "your@email.com" 16 | 17 | p.project = 'ambition' 18 | p.url = "http://ambition.rubyforge.org/" 19 | p.test_pattern = 'test/*_test.rb' 20 | p.version = Version 21 | p.dependencies << 'ambition >=0.5.0' 22 | end 23 | 24 | rescue LoadError 25 | puts "Not doing any of the Echoe gemmy stuff, because you don't have the specified gem versions" 26 | end 27 | 28 | desc 'Install as a gem' 29 | task :install_gem do 30 | puts `rake manifest package && gem install pkg/ambitious-<%= adapter_name %>-#{Version}.gem` 31 | end 32 | -------------------------------------------------------------------------------- /app_generators/ambition_adapter/templates/lib/adapter/base.rb.erb: -------------------------------------------------------------------------------- 1 | module Ambition 2 | module Adapters 3 | module <%= adapter_module %> 4 | class Base 5 | ## 6 | # Extract common functionality into this class. 7 | # All your classes, by default, inherit from this 8 | # one -- Query and the Translators. 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app_generators/ambition_adapter/templates/lib/adapter/query.rb.erb: -------------------------------------------------------------------------------- 1 | =begin 2 | 3 | These methods are king: 4 | 5 | - owner 6 | - clauses 7 | - stash 8 | 9 | +owner+ is the class from which the request was generated. 10 | 11 | User.select { |u| u.name == 'Pork' } 12 | # => owner == User 13 | 14 | +clauses+ is the hash of translated arrays, keyed by processors 15 | 16 | User.select { |u| u.name == 'Pork' } 17 | # => clauses == { :select => [ "users.name = 'Pork'" ] } 18 | 19 | +stash+ is your personal private stash. A hash you can use for 20 | keeping stuff around. 21 | 22 | User.select { |u| u.profile.name == 'Pork' } 23 | # => stash == { :include => [ :profile ] } 24 | 25 | The above is totally arbitrary. It's basically a way for your 26 | translators to talk to each other and, more importantly, to the Query 27 | object. 28 | 29 | =end 30 | module Ambition 31 | module Adapters 32 | module <%= adapter_module %> 33 | class Query < Base 34 | def kick 35 | raise "Example: owner.find(:all, to_hash)" 36 | end 37 | 38 | def size 39 | raise "Example: owner.count(to_hash)" 40 | end 41 | 42 | def to_hash 43 | raise "Not implemented" 44 | end 45 | 46 | def to_s 47 | raise "Not implemented" 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /app_generators/ambition_adapter/templates/lib/adapter/select.rb.erb: -------------------------------------------------------------------------------- 1 | ## 2 | # The format of the documentation herein is: 3 | # 4 | # >> method with block 5 | # => methods on this class called by Ambition (with arguments) 6 | # 7 | module Ambition 8 | module Adapters 9 | module <%= adapter_module %> 10 | class Select < Base 11 | # >> select { |u| u.name == 'chris' } 12 | # => #call(:name) 13 | def call(method) 14 | raise "Not implemented." 15 | end 16 | 17 | # >> select { |u| u.name.downcase == 'chris' } 18 | # => #call(:name, :downcase) 19 | def chained_call(*methods) 20 | # An idiom here is to call the chained method and pass it 21 | # the first method. 22 | # 23 | # if respond_to? methods[1] 24 | # send(methods[1], methods[0]) 25 | # end 26 | # 27 | # In the above example, this translates to calling: 28 | # 29 | # #downcase(:name) 30 | # 31 | raise "Not implemented." 32 | end 33 | 34 | # && 35 | # >> select { |u| u.name == 'chris' && u.age == 22 } 36 | # => #both( processed left side, processed right side ) 37 | def both(left, right) 38 | raise "Not implemented." 39 | end 40 | 41 | # || 42 | # >> select { |u| u.name == 'chris' || u.age == 22 } 43 | # => #either( processed left side, processed right side ) 44 | def either(left, right) 45 | raise "Not implemented." 46 | end 47 | 48 | # >> select { |u| u.name == 'chris' } 49 | # => #==( call(:name), 'chris' ) 50 | def ==(left, right) 51 | raise "Not implemented." 52 | end 53 | 54 | # != 55 | # >> select { |u| u.name != 'chris' } 56 | # => #not_equal( call(:name), 'chris' ) 57 | def not_equal(left, right) 58 | raise "Not implemented." 59 | end 60 | 61 | # >> select { |u| u.name =~ 'chris' } 62 | # => #=~( call(:name), 'chris' ) 63 | def =~(left, right) 64 | raise "Not implemented." 65 | end 66 | 67 | # !~ 68 | # >> select { |u| u.name !~ 'chris' } 69 | # => #not_regexp( call(:name), 'chris' ) 70 | def not_regexp(left, right) 71 | raise "Not implemented." 72 | end 73 | 74 | ## 75 | # Etc. 76 | def <(left, right) 77 | raise "Not implemented." 78 | end 79 | 80 | def >(left, right) 81 | raise "Not implemented." 82 | end 83 | 84 | def >=(left, right) 85 | raise "Not implemented." 86 | end 87 | 88 | def <=(left, right) 89 | raise "Not implemented." 90 | end 91 | 92 | # >> select { |u| [1, 2, 3].include? u.id } 93 | # => #include?( [1, 2, 3], call(:id) ) 94 | def include?(left, right) 95 | raise "Not implemented." 96 | end 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /app_generators/ambition_adapter/templates/lib/adapter/slice.rb.erb: -------------------------------------------------------------------------------- 1 | module Ambition 2 | module Adapters 3 | module <%= adapter_module %> 4 | class Slice < Base 5 | # >> User.first(5) 6 | # => #slice(0, 5) 7 | # 8 | # >> User.first 9 | # => #slice(0, 1) 10 | # 11 | # >> User[10, 20] 12 | # => #slice(10, 20) 13 | def slice(start, length) 14 | raise "Not implemented." 15 | end 16 | end 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app_generators/ambition_adapter/templates/lib/adapter/sort.rb.erb: -------------------------------------------------------------------------------- 1 | module Ambition 2 | module Adapters 3 | module <%= adapter_module %> 4 | class Sort < Base 5 | # >> sort_by { |u| u.age } 6 | # => #sort_by(:age) 7 | def sort_by(method) 8 | raise "Not implemented." 9 | end 10 | 11 | # >> sort_by { |u| -u.age } 12 | # => #reverse_sort_by(:age) 13 | def reverse_sort_by(method) 14 | raise "Not implemented." 15 | end 16 | 17 | # >> sort_by { |u| u.profile.name } 18 | # => #chained_sort_by(:profile, :name) 19 | def chained_sort_by(receiver, method) 20 | raise "Not implemented." 21 | end 22 | 23 | # >> sort_by { |u| -u.profile.name } 24 | # => #chained_reverse_sort_by(:profile, :name) 25 | def chained_reverse_sort_by(receiver, method) 26 | raise "Not implemented." 27 | end 28 | 29 | # >> sort_by(&:name) 30 | # => #to_proc(:name) 31 | def to_proc(symbol) 32 | raise "Not implemented." 33 | end 34 | 35 | # >> sort_by { rand } 36 | # => #rand 37 | def rand 38 | raise "Not implemented." 39 | end 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app_generators/ambition_adapter/templates/lib/init.rb.erb: -------------------------------------------------------------------------------- 1 | require 'ambition' 2 | require '<%= adapter_name %>' 3 | require 'ambition/adapters/<%= adapter_name %>/base' 4 | require 'ambition/adapters/<%= adapter_name %>/query' 5 | require 'ambition/adapters/<%= adapter_name %>/select' 6 | require 'ambition/adapters/<%= adapter_name %>/sort' 7 | require 'ambition/adapters/<%= adapter_name %>/slice' 8 | 9 | ## 10 | # This is where you inject Ambition into your target. 11 | # 12 | # Use `extend' if you are injecting a class, `include' if you are 13 | # injecting instances of that class. 14 | # 15 | # You must also set the `ambition_adapter' class variable on your target 16 | # class, regardless of whether you are injecting instances or the class itself. 17 | # 18 | # You probably want something like this: 19 | # 20 | # <%= adapter_module %>::Base.extend Ambition::API 21 | # <%= adapter_module %>::Base.ambition_adapter = Ambition::Adapters::<%= adapter_module %> 22 | # 23 | -------------------------------------------------------------------------------- /app_generators/ambition_adapter/templates/test/helper.rb.erb: -------------------------------------------------------------------------------- 1 | %w( rubygems test/spec mocha English ).each { |f| require f } 2 | 3 | begin 4 | require 'redgreen' 5 | rescue LoadError 6 | end 7 | 8 | $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib' 9 | require 'ambition/adapters/<%= adapter_name %>' 10 | -------------------------------------------------------------------------------- /app_generators/ambition_adapter/templates/test/select_test.rb.erb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "<%= adapter_module %> Adapter :: Select" do 4 | setup do 5 | @klass = User 6 | end 7 | 8 | xspecify "==" do 9 | translator = @klass.select { |m| m.name == 'jon' } 10 | translator.to_s.should == %Q(foo) 11 | end 12 | 13 | xspecify "!=" do 14 | translator = @klass.select { |m| m.name != 'jon' } 15 | translator.to_s.should == %Q(foo) 16 | end 17 | 18 | xspecify "== && ==" do 19 | translator = @klass.select { |m| m.name == 'jon' && m.age == 21 } 20 | translator.to_s.should == %Q(foo) 21 | end 22 | 23 | xspecify "== || ==" do 24 | translator = @klass.select { |m| m.name == 'jon' || m.age == 21 } 25 | translator.to_s.should == %Q(foo) 26 | end 27 | 28 | xspecify "mixed && and ||" do 29 | translator = @klass.select { |m| m.name == 'jon' || m.age == 21 && m.password == 'pass' } 30 | translator.to_s.should == %Q(foo) 31 | end 32 | 33 | xspecify "grouped && and ||" do 34 | translator = @klass.select { |m| (m.name == 'jon' || m.name == 'rick') && m.age == 21 } 35 | translator.to_s.should == %Q(foo) 36 | end 37 | 38 | xspecify ">/<" do 39 | translator = @klass.select { |m| m.age > 21 } 40 | translator.to_s.should == %Q(foo) 41 | 42 | translator = @klass.select { |m| m.age >= 21 } 43 | translator.to_s.should == %Q(foo) 44 | 45 | translator = @klass.select { |m| m.age < 21 } 46 | translator.to_s.should == %Q(foo) 47 | end 48 | 49 | xspecify "array.include? item" do 50 | translator = @klass.select { |m| [1, 2, 3, 4].include? m.id } 51 | translator.to_s.should == %Q(foo) 52 | end 53 | 54 | xspecify "variabled array.include? item" do 55 | array = [1, 2, 3, 4] 56 | translator = @klass.select { |m| array.include? m.id } 57 | translator.to_s.should == %Q(foo) 58 | end 59 | 60 | xspecify "== with variables" do 61 | me = 'chris' 62 | translator = @klass.select { |m| m.name == me } 63 | translator.to_s.should == %Q(foo) 64 | end 65 | 66 | xspecify "== with method arguments" do 67 | def test_it(name) 68 | translator = @klass.select { |m| m.name == name } 69 | translator.to_s.should == %Q(foo) 70 | end 71 | 72 | test_it('chris') 73 | end 74 | 75 | xspecify "== with instance variables" do 76 | @me = 'chris' 77 | translator = @klass.select { |m| m.name == @me } 78 | translator.to_s.should == %Q(foo) 79 | end 80 | 81 | xspecify "== with instance variable method call" do 82 | require 'ostruct' 83 | @person = OpenStruct.new(:name => 'chris') 84 | 85 | translator = @klass.select { |m| m.name == @person.name } 86 | translator.to_s.should == %Q(foo) 87 | end 88 | 89 | xspecify "== with global variables" do 90 | $my_name = 'boston' 91 | translator = @klass.select { |m| m.name == $my_name } 92 | translator.to_s.should == %Q(foo) 93 | end 94 | 95 | xspecify "== with method call" do 96 | def band 97 | 'skinny puppy' 98 | end 99 | 100 | translator = @klass.select { |m| m.name == band } 101 | translator.to_s.should == %Q(foo) 102 | end 103 | 104 | xspecify "=~ with string" do 105 | translator = @klass.select { |m| m.name =~ 'chris' } 106 | translator.to_s.should == %Q(foo) 107 | 108 | translator = @klass.select { |m| m.name =~ 'chri%' } 109 | translator.to_s.should == %Q(foo) 110 | end 111 | 112 | xspecify "!~ with string" do 113 | translator = @klass.select { |m| m.name !~ 'chris' } 114 | translator.to_s.should == %Q(foo) 115 | 116 | translator = @klass.select { |m| !(m.name =~ 'chris') } 117 | translator.to_s.should == %Q(foo) 118 | end 119 | 120 | xspecify "=~ with regexp" do 121 | translator = @klass.select { |m| m.name =~ /chris/ } 122 | translator.to_s.should == %Q(foo) 123 | end 124 | 125 | xspecify "=~ with regexp flags" do 126 | translator = @klass.select { |m| m.name =~ /chris/i } 127 | translator.to_s.should == %Q(foo) 128 | end 129 | 130 | xspecify "downcase" do 131 | translator = @klass.select { |m| m.name.downcase =~ 'chris%' } 132 | translator.to_s.should == %Q(foo) 133 | end 134 | 135 | xspecify "upcase" do 136 | translator = @klass.select { |m| m.name.upcase =~ 'chris%' } 137 | translator.to_s.should == %Q(foo) 138 | end 139 | 140 | xspecify "undefined equality symbol" do 141 | should.raise { @klass.select { |m| m.name =* /chris/ } } 142 | end 143 | 144 | xspecify "block variable / assigning variable conflict" do 145 | m = @klass.select { |m| m.name == 'chris' } 146 | m.should == %Q(foo) 147 | end 148 | 149 | xspecify "== with inline ruby" do 150 | translator = @klass.select { |m| m.created_at == Time.now.to_s } 151 | translator.to_s.should == %Q(foo) 152 | end 153 | 154 | xspecify "inspect" do 155 | @klass.select { |u| u.name }.inspect.should.match %r(call #to_s or #to_hash) 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /app_generators/ambition_adapter/templates/test/slice_test.rb.erb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "<%= adapter_module %> Adapter :: Slice" do 4 | setup do 5 | @klass = User 6 | @block = @klass.select { |m| m.name == 'jon' } 7 | end 8 | 9 | xspecify "first" do 10 | @klass.expects(:find).with(:limit => 1, :name => 'jon') 11 | @block.first 12 | end 13 | 14 | xspecify "first with argument" do 15 | @klass.expects(:find).with(:limit => 5, :name => 'jon') 16 | @block.first(5).entries 17 | end 18 | 19 | xspecify "[] with two elements" do 20 | @klass.expects(:find).with(:limit => 20, :offset => 10, :name => 'jon') 21 | @block[10, 20].entries 22 | 23 | @klass.expects(:find).with(:limit => 20, :offset => 20, :name => 'jon') 24 | @block[20, 20].entries 25 | end 26 | 27 | xspecify "slice is an alias of []" do 28 | @klass.expects(:find).with(:limit => 20, :offset => 10, :name => 'jon') 29 | @block.slice(10, 20).entries 30 | end 31 | 32 | xspecify "[] with range" do 33 | @klass.expects(:find).with(:limit => 10, :offset => 10, :name => 'jon') 34 | @block[11..20].entries 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /app_generators/ambition_adapter/templates/test/sort_test.rb.erb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "<%= adapter_module %> Adapter :: Sort" do 4 | setup do 5 | @klass = User 6 | @block = @klass.select { |m| m.name == 'jon' } 7 | end 8 | 9 | xspecify "order" do 10 | string = @block.sort_by { |m| m.name }.to_s 11 | string.should == "foo" 12 | end 13 | 14 | xspecify "combined order" do 15 | string = @block.sort_by { |m| [ m.name, m.age ] }.to_s 16 | string.should == "foo" 17 | end 18 | 19 | xspecify "combined order with single reverse" do 20 | string = @block.sort_by { |m| [ m.name, -m.age ] }.to_s 21 | string.should == "foo" 22 | end 23 | 24 | xspecify "combined order with two reverses" do 25 | string = @block.sort_by { |m| [ -m.name, -m.age ] }.to_s 26 | string.should == "foo" 27 | end 28 | 29 | xspecify "reverse order with -" do 30 | string = @block.sort_by { |m| -m.age }.to_s 31 | string.should == "foo" 32 | end 33 | 34 | xspecify "reverse order with #reverse" do 35 | # TODO: not implemented 36 | string = @block.sort_by { |m| m.age }.reverse.to_s 37 | string.should == "foo" 38 | end 39 | 40 | xspecify "random order" do 41 | string = @block.sort_by { rand }.to_s 42 | string.should == "foo" 43 | end 44 | 45 | xspecify "non-existent method to sort by" do 46 | should.raise(NoMethodError) { @block.sort_by { foo }.to_s } 47 | end 48 | 49 | xspecify "Symbol#to_proc" do 50 | string = @klass.sort_by(&:name).to_s 51 | string.should == "foo" 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /bin/ambition_adapter: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'rubygems' 3 | require 'rubigen' 4 | 5 | if %w(-v --version).include? ARGV.first 6 | version = File.read('./Rakefile').scan(/Version = '(.+)'/).first.first 7 | puts "#{File.basename($0)} #{version}" 8 | exit(0) 9 | end 10 | 11 | require 'rubigen/scripts/generate' 12 | RubiGen::Base.use_application_sources! :ambition_adapter 13 | RubiGen::Scripts::Generate.new.run(ARGV, :generator => 'ambition_adapter') 14 | -------------------------------------------------------------------------------- /deps.rip: -------------------------------------------------------------------------------- 1 | git://github.com/drnic/rubigen.git REL-1.3.0 2 | git://github.com/seattlerb/ruby2ruby.git e3cf57559 # 1.1.8 3 | git://github.com/seattlerb/parsetree.git 480ede9d9 # 2.1.1 4 | -------------------------------------------------------------------------------- /lib/ambition.rb: -------------------------------------------------------------------------------- 1 | require 'ambition/enumerable' 2 | require 'ambition/api' 3 | require 'ambition/context' 4 | require 'ambition/core_ext' 5 | require 'ambition/sexp_translator' 6 | 7 | require 'ambition/processors/base' 8 | require 'ambition/processors/select' 9 | require 'ambition/processors/sort' 10 | require 'ambition/processors/slice' 11 | require 'ambition/processors/ruby' 12 | -------------------------------------------------------------------------------- /lib/ambition/api.rb: -------------------------------------------------------------------------------- 1 | module Ambition #:nodoc: 2 | # Module that you will extend from in your adapters in your toplevel file. 3 | # 4 | # For example, for ambitious_sphinx in lib/ambition/adapters/ambitious_sphinx.rb, we have: 5 | # ActiveRecord::Base.extend Ambition::API 6 | module API 7 | include Enumerable 8 | 9 | ## 10 | # Entry methods 11 | def select(&block) 12 | context = ambition_context 13 | context << Processors::Select.new(context, block) 14 | end 15 | 16 | def sort_by(&block) 17 | context = ambition_context 18 | context << Processors::Sort.new(context, block) 19 | end 20 | 21 | # Entries that our context is able to find. 22 | def entries 23 | ambition_context.kick 24 | end 25 | alias_method :to_a, :entries 26 | 27 | def size 28 | ambition_context == self ? super : ambition_context.size 29 | end 30 | 31 | def slice(start, length = nil) 32 | context = ambition_context 33 | context << Processors::Slice.new(context, start, length) 34 | end 35 | alias_method :[], :slice 36 | 37 | ## 38 | # Convenience methods 39 | 40 | # See Enumerable#detect 41 | def detect(&block) 42 | select(&block).first 43 | end 44 | 45 | # See Array#first 46 | def first(count = 1) 47 | sliced = slice(0, count) 48 | count == 1 ? Array(sliced.kick).first : sliced 49 | end 50 | 51 | # See Array#each, applied to +entries+ 52 | def each(&block) 53 | entries.each(&block) 54 | end 55 | 56 | # See Enumerable#any? 57 | def any?(&block) 58 | select(&block).size > 0 59 | end 60 | 61 | # See Enumerable#all? 62 | def all?(&block) 63 | size == select(&block).size 64 | end 65 | 66 | # See Array#empty? 67 | def empty? 68 | size.zero? 69 | end 70 | 71 | # Builds a new +Context+. 72 | def ambition_context 73 | Context.new(self) 74 | end 75 | 76 | # Gives you the current ambitious adapter. 77 | def ambition_adapter 78 | name = respond_to?(:name) ? name : self.class.name 79 | parent = respond_to?(:superclass) ? superclass : self.class.superclass 80 | @@ambition_adapter[name] || @@ambition_adapter[parent.name] 81 | end 82 | 83 | # Assign the ambition adapter. Typically, you use this in the toplevel file of your adapter. 84 | # 85 | # For example, for ambitious_sphinx, in our lib/ambition/adapters/ambitious_sphinx.rb: 86 | # 87 | # ActiveRecord::Base.ambition_adapter = Ambition::Adapters::AmbitiousSphinx 88 | def ambition_adapter=(klass) 89 | @@ambition_adapter ||= {} 90 | # should this be doing the same check for respond_to?(:name) like above? 91 | @@ambition_adapter[name] = klass 92 | end 93 | 94 | def ambition_owner 95 | @owner || self 96 | end 97 | end 98 | end 99 | -------------------------------------------------------------------------------- /lib/ambition/context.rb: -------------------------------------------------------------------------------- 1 | module Ambition #:nodoc: 2 | # This class includes several methods you will likely want to be accessing through your 3 | # Query and Translator classes: 4 | # 5 | # * +clauses+ 6 | # * +owner+ 7 | # * +stash+ 8 | class Context 9 | undef_method :to_s 10 | include API 11 | 12 | # A hash of arrays, one key per processor. 13 | # So, if someone called User.select, your 14 | # +clauses+ hash would have a :select key with 15 | # an array of translated strings via your Select 16 | # class. 17 | # 18 | # This is accessible from your Query and Translator classes. 19 | attr_reader :clauses 20 | 21 | # The class everything was called on. Like `User` 22 | # 23 | # This is accessible from your Query and Translator classes. 24 | attr_reader :owner 25 | 26 | # A place for you to stick stuff. Available to all Translators and your Query class. 27 | # 28 | # This is accessible from your Query and Translator classes. 29 | attr_reader :stash 30 | 31 | def initialize(owner) 32 | @owner = owner 33 | @clauses = {} 34 | @stash = {} 35 | end 36 | 37 | # Gets the ambition_context. From a Ambition::Context, this is actually +self+. 38 | def ambition_context 39 | self 40 | end 41 | 42 | # Adds a clause to this context. 43 | def <<(clause) 44 | @clauses[clause.key] ||= [] 45 | @clauses[clause.key] << clause.to_s 46 | self 47 | end 48 | 49 | def adapter_query 50 | Processors::Base.translator(self, :Query) 51 | end 52 | 53 | def method_missing(method, *args, &block) 54 | return super unless adapter_query.respond_to? method 55 | adapter_query.send(method, *args, &block) 56 | end 57 | 58 | def inspect 59 | "(Query object: call #to_s or #to_hash to inspect, call an Enumerable (such as #each or #first) to request data)" 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/ambition/core_ext.rb: -------------------------------------------------------------------------------- 1 | # Object extensions to make metaprogramming a little easier. 2 | class Object 3 | def metaclass; (class << self; self end) end 4 | def meta_eval(&blk) metaclass.instance_eval(&blk) end 5 | def meta_def(name, &blk) meta_eval { define_method name, &blk } end 6 | def class_def(name, &blk) class_eval { define_method name, &blk } end 7 | end 8 | -------------------------------------------------------------------------------- /lib/ambition/enumerable.rb: -------------------------------------------------------------------------------- 1 | module Ambition #:nodoc: 2 | Enumerable = ::Enumerable.dup 3 | Enumerable.class_eval do 4 | remove_method :find, :find_all 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/ambition/processors/base.rb: -------------------------------------------------------------------------------- 1 | module Ambition #:nodoc: 2 | module Processors #:nodoc: 3 | class Base 4 | ## 5 | # Processing methods 6 | def process_proc(exp) 7 | # puts "=> #{exp.inspect}" 8 | receiver, body = process(exp.shift), exp.shift 9 | process(body) 10 | end 11 | 12 | def process_dasgn_curr(exp) 13 | (@receiver = exp.first).to_s 14 | end 15 | alias_method :process_dasgn, :process_dasgn_curr 16 | 17 | def process_array(exp) 18 | # Branch on whether this is straight Ruby or a real array 19 | rubify(exp) || exp.map { |m| process(m) } 20 | end 21 | 22 | def process_str(exp) 23 | exp.first 24 | end 25 | 26 | def process_lit(exp) 27 | exp.first 28 | end 29 | 30 | def process_nil(exp) 31 | nil 32 | end 33 | 34 | def process_true(exp) 35 | true 36 | end 37 | 38 | def process_false(exp) 39 | false 40 | end 41 | 42 | def process_dvar(exp) 43 | target = exp.shift 44 | value(target.to_s[0..-1]) 45 | end 46 | 47 | def process_ivar(exp) 48 | value(exp.shift.to_s[0..-1]) 49 | end 50 | 51 | def process_lvar(exp) 52 | value(exp.shift.to_s) 53 | end 54 | 55 | def process_vcall(exp) 56 | value(exp.shift.to_s) 57 | end 58 | 59 | def process_gvar(exp) 60 | value(exp.shift.to_s) 61 | end 62 | 63 | def process(node) 64 | node ||= [] 65 | 66 | if node.is_a? Symbol 67 | node 68 | elsif respond_to?(method = "process_#{node.first}") 69 | send(method, node[1..-1]) 70 | elsif node.blank? 71 | '' 72 | else 73 | raise "Missing process method for sexp: #{node.inspect}" 74 | end 75 | end 76 | 77 | ## 78 | # Helper methods 79 | def to_s 80 | process SexpTranslator.translate(@block) 81 | end 82 | 83 | def key 84 | self.class.name.split('::').last.downcase.intern 85 | end 86 | 87 | def value(variable) 88 | eval variable, @block 89 | end 90 | 91 | # Gives you the current translator. Uses +self.translator+ to look it up, 92 | # if it isn't known yet. 93 | def translator 94 | @translator ||= self.class.translator(@context) 95 | end 96 | 97 | def self.translator(context, name = nil) 98 | # Grok the adapter name 99 | name ||= self.name.split('::').last 100 | 101 | # Get the module for it 102 | klass = context.owner.ambition_adapter.const_get(name) 103 | instance = klass.new 104 | 105 | # Make sure that the instance has everything it will need: 106 | # 107 | # * context 108 | # * owner 109 | # * clauses 110 | # * stash 111 | # * negated? 112 | unless instance.respond_to? :context 113 | klass.class_eval do 114 | attr_accessor :context, :negated 115 | def owner; @context.owner end 116 | def clauses; @context.clauses end 117 | def stash; @context.stash end 118 | def negated?; @negated end 119 | end 120 | end 121 | 122 | instance.context = context 123 | instance 124 | end 125 | 126 | def rubify(exp) 127 | # TODO: encapsulate this check in Ruby.should_process?(exp) 128 | if exp.first.first == :call && exp.first[1].last != @receiver && Array(exp.first[1][1]).last != @receiver 129 | value Ruby.process(exp.first) 130 | end 131 | end 132 | end 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /lib/ambition/processors/ruby.rb: -------------------------------------------------------------------------------- 1 | require 'ruby2ruby' 2 | 3 | module Ambition #:nodoc: 4 | module Processors #:nodoc: 5 | class Ruby < RubyToRuby 6 | def self.process(node) 7 | @processor ||= new 8 | @processor.process node 9 | end 10 | 11 | ## 12 | # This is not DRY, and I don't care. 13 | def process(node) 14 | node ||= [] 15 | 16 | if respond_to?(method = "process_#{node.first}") 17 | send(method, node[1..-1]) 18 | elsif node.blank? 19 | '' 20 | else 21 | raise "Missing process method for sexp: #{node.inspect}" 22 | end 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/ambition/processors/select.rb: -------------------------------------------------------------------------------- 1 | module Ambition #:nodoc: 2 | module Processors #:nodoc: 3 | class Select < Base 4 | def initialize(context, block) 5 | @context = context 6 | @block = block 7 | end 8 | 9 | def process_call(args) 10 | # Operation (m.name == 'chris') 11 | # [[:call, [:dvar, :m], :name], :==, [:array, [:str, "chris"]]] 12 | if args.size == 3 13 | left, operator, right = args 14 | 15 | # params are passed as an array, even when only one element: 16 | # abc(1) 17 | # => [:fcall, :abc, [:array, [:lit, 1]] 18 | # abc([1]) 19 | # => [:fcall, :abc, [:array, [:array, [:lit, 1]]]] 20 | if right.first == :array 21 | right = process(right) 22 | right = right.is_a?(Array) ? right.first : right 23 | else 24 | right = process(right) 25 | end 26 | 27 | translator.send(process_operator(operator), process(left), right) 28 | 29 | # Property of passed arg: 30 | # [[:dvar, :m], :name] 31 | elsif args.first.last == @receiver 32 | translator.call(*args[1..-1]) 33 | 34 | # Method call: 35 | # [[:call, [:dvar, :m], :name], :upcase] 36 | elsif args.first.first == :call && args.first[1].last == @receiver 37 | receiver, method = args 38 | translator.chained_call(receiver.last, method) 39 | 40 | # Deep, chained call: 41 | # [[:call, [:call, [:call, [:dvar, :m], :created_at], :something], :else], :perhaps] 42 | elsif args.flatten.include? @receiver 43 | calls = [] 44 | 45 | until args.empty? 46 | args = args.last if args.last.is_a?(Array) 47 | break if args.last == @receiver 48 | calls << args.pop 49 | end 50 | 51 | translator.chained_call(*calls.reverse) 52 | 53 | else 54 | raise args.inspect 55 | end 56 | end 57 | 58 | def process_match3(exp) 59 | right, left = exp 60 | process_call [ left, :=~, right ] 61 | end 62 | 63 | def process_and(exp) 64 | joined_expressions exp, :both 65 | end 66 | 67 | def process_or(exp) 68 | joined_expressions exp, :either 69 | end 70 | 71 | def joined_expressions(exp, with = nil) 72 | expressions = [] 73 | 74 | while expression = exp.shift 75 | expressions << process(expression) 76 | end 77 | 78 | translator.send(with, *expressions) 79 | end 80 | 81 | def process_not(args) 82 | negate { process(args.first) } 83 | end 84 | 85 | def process_operator(operator) 86 | @negated ? negate_operator(operator) : operator 87 | end 88 | 89 | def negate_operator(operator) 90 | case operator 91 | when :== then :not_equal 92 | when :=~ then :not_regexp 93 | else raise "Missing negated operator definition: #{operator}" 94 | end 95 | end 96 | 97 | def negate 98 | @negated = translator.negated = true 99 | yield 100 | ensure 101 | @negated = translator.negated = false 102 | end 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /lib/ambition/processors/slice.rb: -------------------------------------------------------------------------------- 1 | module Ambition #:nodoc: 2 | module Processors #:nodoc: 3 | class Slice < Base 4 | def initialize(context, start, length=nil) 5 | @context = context 6 | @start = start 7 | @length = length 8 | end 9 | 10 | def to_s 11 | translator.slice(@start, @length) 12 | end 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/ambition/processors/sort.rb: -------------------------------------------------------------------------------- 1 | module Ambition #:nodoc: 2 | module Processors #:nodoc: 3 | class Sort < Base 4 | def initialize(context, block) 5 | @context = context 6 | @block = block 7 | end 8 | 9 | def process_call(args) 10 | if args.first.last == @receiver 11 | translator.sort_by(*args[1..-1]) 12 | 13 | # sort_by { |m| -m.age } 14 | # [[:call, [:dvar, :m], :age], :-@] 15 | elsif args[0][1][-1] == @receiver && args.last == :-@ 16 | translator.reverse_sort_by(*args.first[2..-1]) 17 | 18 | # sort_by(&:name).to_s 19 | # [[:call, [:dvar, :args], :shift], :__send__, [:argscat, [:array, [:self]], [:dvar, :args]]] 20 | elsif args[1] == :__send__ 21 | translator.to_proc(value('to_s')) 22 | 23 | # sort_by { |m| m.ideas.title } 24 | # [[:call, [:dvar, :m], :ideas], :title] 25 | elsif args[0][1][-1] == @receiver 26 | first = args.first.last 27 | last = args.last 28 | translator.chained_sort_by(first, last) 29 | 30 | # sort_by { |m| [ m.ideas.title, -m.invites.email ] } 31 | # [[:call, [:call, [:dvar, :m], :invites], :email], :-@] 32 | elsif args[0][1][1][-1] == @receiver && args.last == :-@ 33 | first = args.first[1].last 34 | last = args.first.last 35 | translator.chained_reverse_sort_by(first, last) 36 | 37 | else 38 | raise args.inspect 39 | end 40 | end 41 | 42 | def process_vcall(args) 43 | translator.send(args.shift, *args) 44 | end 45 | 46 | def process_masgn(exp) 47 | '' 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/ambition/sexp_translator.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'parse_tree' 3 | 4 | module Ambition #:nodoc: 5 | class SexpTranslator 6 | @@block_cache = {} 7 | 8 | def self.translate(block) 9 | @@block_cache[block.to_s] ||= begin 10 | klass = Class.new { define_method(:proc_to_method, block) } 11 | body = ParseTree.translate(klass, :proc_to_method)[2][1..-1] 12 | [:proc, *body] 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /site/Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'redcloth' 3 | require 'rake' 4 | require 'rake/contrib/sshpublisher' 5 | 6 | StaticFiles = %w( png gif jpeg jpg css js ) 7 | LiveDirectories = %w( adapters textile static ) 8 | 9 | task :default => [ :clean, :build ] 10 | 11 | desc "Clean generated files" 12 | task :clean do 13 | FileUtils.rm_rf 'live' 14 | end 15 | 16 | def compile(file) 17 | layout = File.read('src/layout.textile') 18 | layout.sub('%CONTENT%', compile_without_layout(file)).gsub('%PAGE%', File.basename(file).split('.').first) 19 | end 20 | 21 | def compile_without_layout(file) 22 | textile = File.read(file). 23 | gsub(/%INCLUDE (.+)%/) { |match| File.read("src/#{$1}") }. 24 | gsub('', '
').gsub('', '
') 25 | RedCloth.new(textile).to_html 26 | end 27 | 28 | desc "Build live HTML files" 29 | task :build do 30 | LiveDirectories.each { |dir| FileUtils.mkdir_p "live/#{dir}" } 31 | Dir['src/**/*.textile'].each do |file| 32 | next if File.basename(file) == 'layout.textile' 33 | 34 | file_name = file.sub('src/', '').sub('.textile', '.html') 35 | live_file = "live/#{file_name}" 36 | 37 | unless uptodate?(live_file, file) && uptodate?(live_file, "src/layout.textile") 38 | File.open(live_file, 'w') { |f| f.puts(compile(file)); f.flush } 39 | puts "created #{live_file}" 40 | cp file, 'live/textile/' + file.split('/').last.sub('.textile', '.txt') 41 | end 42 | end 43 | 44 | static_files = "{#{StaticFiles.join(',')}}" 45 | Dir["src/**/*.#{static_files}"].each do |file| 46 | file = file.sub('src/','') 47 | src, live = "src/#{file}", "live/#{file}" 48 | unless uptodate? live, src 49 | cp src, live 50 | puts "copied #{file}" 51 | end 52 | end 53 | end 54 | 55 | desc "Publish the documentation" 56 | task :publish => [ :build ] do 57 | Rake::SshDirPublisher.new( 58 | "defunkt@rubyforge.org", 59 | "/var/www/gforge-projects/ambition/", 60 | "live" ).upload 61 | end 62 | -------------------------------------------------------------------------------- /site/src/_adapters.textile: -------------------------------------------------------------------------------- 1 | Adapters are gems named @ambitious-something@, where _something_ corresponds to the data 2 | store they are adapting. They can be required in code via @ambition/adapters/something@. 3 | 4 | To install and test the ActiveRecord adapter: 5 | 6 |
 7 | $ gem install ambitious-activerecord
 8 | $ irb 
 9 | >> require 'rubygems'
10 | >> require 'ambition/adapters/active_record'
11 | 
12 | 13 | Adapters typically inject themselves into their target automatically, so that should be 14 | all you need. 15 | 16 | There are a few adapters in development or released currently: 17 | 18 | * "ActiveRecord":adapters/activerecord.html 19 | * ActiveLDAP 20 | * Facebook 21 | * XPath 22 | * CouchDB 23 | * DataMapper 24 | -------------------------------------------------------------------------------- /site/src/adapters.textile: -------------------------------------------------------------------------------- 1 | h2. The Adapters 2 | 3 | %INCLUDE _adapters.textile% 4 | 5 | If you're interested in writing your own adapter, read on. 6 | 7 | h2. The Anatomy of an Adapter 8 | 9 | Ambition adapters consist of two parts: *Translators* and the *Query*. Translators 10 | are used to translate plane jane Ruby into strings while the Query is used to build 11 | and execute a query from those strings. 12 | 13 | The three translators are @Select@, @Slice@, and @Sort@. Their names correspond to the 14 | API method they represent. Each translator consists of methods which convert passed 15 | arguments into a string. 16 | 17 | Here's how the ActiveRecord adapter maps translator classes to SQL clauses: 18 | 19 | * @Select@ => @WHERE@ 20 | * @Slice@ => @LIMIT@ and @OFFSET@ 21 | * @Sort@ => @ORDER BY@ 22 | 23 | Your translators and the Query have three special methods available at all times: 24 | 25 | * @owner@ 26 | * @clauses@ 27 | * @stash@ 28 | 29 | @owner@ is the class from which the request was generated. 30 | 31 | 32 | User.select { |u| u.name == 'Pork' } 33 | # => owner == User 34 | 35 | 36 | @clauses@ is the hash of translated string arrays, keyed by processors. 37 | 38 | 39 | User.select { |u| u.name == 'Pork' } 40 | # => clauses == { :select => [ "users.name = 'Pork'" ] } 41 | 42 | 43 | @stash@ is your personal private stash. A hash you can use for 44 | keeping stuff around. Translators are free to set things which 45 | can later be picked up by the Query class. 46 | 47 | For instance, the @ActiveRecord@ adapter's @Select@ translator adds to the 48 | @stash[:include]@ array whenever it thinks it needs to do a join. The 49 | Query class picks this up and adds it to the hash it feeds 50 | @find(:all)@. 51 | 52 | 53 | User.select { |u| u.profile.name == 'Pork' } 54 | # => stash == { :include => [ :profile ] } 55 | 56 | 57 | @stash@ is basically a way for your translators to talk to each other and, 58 | more importantly, to the Query. 59 | 60 | The Query is what kicks off the actual data store query, after all the translators have done 61 | their business. Its @clauses@ and @stash@ hashes are as full as they will ever be. 62 | 63 | The kicking is done by one of two methods: 64 | 65 | * @kick@ 66 | * @count@ 67 | 68 | While two other methods are generally expected to turn the @clauses@ hash into something 69 | semantically valid: 70 | 71 | * @to_s@ 72 | * @to_hash@ 73 | 74 | So, for instance, @Query#kick@ may look like this: 75 | 76 | 77 | def kick 78 | owner.find(:all, to_hash) 79 | end 80 | 81 | 82 | A straightforward translator/query API reference can be found on the "api":api.html page. 83 | 84 | The easiest way to understand translators is to check out the source of the existing adapters 85 | or by using the adapter generator. 86 | 87 | h2. The Adapter Generator 88 | 89 | Ambition ships with an @ambition_adapter@ script which can generate an adapter skeleton. Built 90 | using Dr Nic's "Rubigen":http://rubigen.rubyforge.org/, it spits out all the files, tests, and 91 | Rakefiles your adapter needs. 92 | 93 | Run it: 94 | 95 |
 96 | $ ambition_adapter flickr
 97 |       create  
 98 |       create  lib/ambition/adapters/flickr
 99 |       create  test
100 |       create  lib/ambition/adapters/flickr/base.rb
101 |       create  lib/ambition/adapters/flickr/query.rb
102 |       create  lib/ambition/adapters/flickr/select.rb
103 |       create  lib/ambition/adapters/flickr/slice.rb
104 |       create  lib/ambition/adapters/flickr/sort.rb
105 |       create  lib/ambition/adapters/flickr.rb
106 |       create  test/helper.rb
107 |       create  test/select_test.rb
108 |       create  test/slice_test.rb
109 |       create  test/sort_test.rb
110 |       create  README
111 |       create  Rakefile
112 | 
113 | 114 | Presto, you've got a ready and willing adapter skeleton in place now. Check out the comments 115 | and you're on your way. 116 | 117 | h2. The Flow: Ambition + Adapters 118 | 119 | Let us examine the flow of a typical call, using the @ActiveRecord@ adapter for reference. 120 | 121 | The call: 122 | 123 | 124 | User.select { |u| u.name == 'Chris' && u.age == 22 }.to_s 125 | 126 | 127 | The first few steps: 128 | 129 | * @Ambition::API#select@ is called. 130 | * An @Ambition::Context@ is created. 131 | * An @Ambition::Processors::Select@ is created and added to the context. 132 | * The context calls @to_s@ on the new @Select@ processor 133 | * The block passed to @select@ is processed. 134 | 135 | This processing is the real meat. Ambition will instantiate your @Select@ translator and 136 | pass values to it, saving the return value of these method calls. 137 | 138 | * @Ambition::Adapters::ActiveRecord::Select@ is instantiated. 139 | * The translator's @call@ method is passed @:name@, returning @"users.name"@ 140 | * The translator's @==@ method is passed @"users.name"@ and @"Chris"@, returning @"users.name = 'Chris'"@ 141 | * @call@ is passed @:age@, returning @"users.age"@ 142 | * @==@ is passed @"users.age"@ and @22@, returning @"users.age = 22"@ 143 | * The translator's @both@ method is passed @"users.name = 'Chris'"@ and @"users.age = 22"@, 144 | returning @"(users.name = 'Chris' AND users.age = 22)"@ 145 | 146 | At this point we leave adapter-land. The final string is stored in the @clauses@ hash 147 | (available to your @Query@) by the context. The @clauses@ hash is keyed by the translator 148 | name -- in this case, @:select@. 149 | 150 | * The context is returned by @Ambition::API#select@. 151 | * @to_s@ is called on the context 152 | * The context forwards this @to_s@ call to an instance of the adapter's @Query@ class 153 | * The ActiveRecord adapter's @to_s@ calls @to_hash@ 154 | * @to_hash@ uses the @clauses@ hash to build an AR hash 155 | * @to_s@ then uses the hash's members to build a SQL string 156 | 157 | The final string is then returned: 158 | 159 | 160 | "SELECT * FROM users WHERE (users.name = 'Chris' AND users.age = 22)" 161 | 162 | 163 | And that's all there is to it. Except, of course, for the "api":api.html page. 164 | -------------------------------------------------------------------------------- /site/src/adapters/activerecord.textile: -------------------------------------------------------------------------------- 1 | h2. An Ambitious ActiveRecord Adapter 2 | 3 | I could tell you all about how awesome the internals are, or 4 | how fun it was to write, or how it'll make you rich and famous, 5 | but instead I'm just going to show you some examples. 6 | 7 | h2. Get It 8 | 9 | @$ sudo gem install ambitious-activerecord@ 10 | 11 | This will suck in the adapter and its dependencies (ActiveRecord & Ambition). 12 | It's fully usable outside of Rails (I use it in a Camping app or two), as long 13 | as you're riding ActiveRecord. 14 | 15 | Now require it in your app: 16 | 17 |
 18 | require 'rubygems'
 19 | require 'ambition/adapters/active_record'
 20 | 
21 | 22 | h2. Examples 23 | 24 | Basically, you write your SQL in Ruby. No, not in Ruby. As Ruby. 25 | 26 | 27 | User.select { |u| u.city == 'San Francisco' }.each do |user| 28 | puts user.name 29 | end 30 | 31 | 32 | And that's it. 33 | 34 | The key is that queries aren't actually run until the data they represent is 35 | requested. Usually this is done with what I call a kicker method. You can call them 36 | that, too. 37 | 38 | Kicker methods are guys like @detect@, @each@, @each_with_index@, @map@, @entries@, 39 | @to_a@, and @first@ (with no argument). Methods like @select@, @sort_by@, and @first@ 40 | (with an argument) are not kicker methods and return a @Context@ object without running any SQL. 41 | 42 | Our @Context@ object has two useful methods: @to_s@ and @to_hash@. With these, 43 | we can check out what exactly we're building. Not everyone has @to_s@, 44 | though. Mostly ignore these methods and treat everything like you normally 45 | would. 46 | 47 | See, @to_s@: 48 | 49 | 50 | >> User.select { |m| m.name == 'jon' }.to_s 51 | => "SELECT * FROM users WHERE users.name = 'jon'" 52 | 53 | 54 | See, @to_hash@: 55 | 56 | 57 | >> User.select { |m| m.name == 'jon' }.to_hash 58 | => { :conditions => "users.name = 'jon'" } 59 | 60 | 61 | h2. Equality - select { |u| u.field == 'bob' } 62 | 63 | 64 | User.select { |m| m.name == 'jon' } 65 | "SELECT * FROM users WHERE users.name = 'jon'" 66 | 67 | User.select { |m| m.created_at > 2.days.ago } 68 | "SELECT * FROM users WHERE users.created_at > '2007-09-26 20:37:47'" 69 | 70 | User.select { |m| m.name == 'jon' } 71 | "SELECT * FROM users WHERE users.name = 'jon'" 72 | 73 | User.select { |m| m.name != 'jon' } 74 | "SELECT * FROM users WHERE users.name <> 'jon'" 75 | 76 | User.select { |m| m.name == 'jon' && m.age == 21 } 77 | "SELECT * FROM users WHERE (users.name = 'jon' AND users.age = 21)" 78 | 79 | User.select { |m| m.name == 'jon' || m.age == 21 } 80 | "SELECT * FROM users WHERE (users.name = 'jon' OR users.age = 21)" 81 | 82 | User.select { |m| m.name == 'jon' || m.age == 21 && m.password == 'pass' } 83 | "SELECT * FROM users WHERE 84 | (users.name = 'jon' OR (users.age = 21 AND users.password = 'pass'))" 85 | 86 | User.select { |m| (m.name == 'jon' || m.name == 'rick') && m.age == 21 } 87 | "SELECT * FROM users WHERE 88 | ((users.name = 'jon' OR users.name = 'rick') AND users.age = 21)" 89 | 90 | 91 | h2. Associations - select { |u| u.field == 'bob' && u.association.field == 'bob@bob.com' } 92 | 93 | The @to_s@ method doesn't work on associations yet, but that's okay: they can 94 | still query through ActiveRecord just fine. 95 | 96 | 97 | User.select do |u| 98 | u.email == 'chris@ozmm.org' && u.profile.name == 'chris wanstrath' 99 | end.map(&:title) 100 | 101 | "SELECT users.id AS t0_r0, ... FROM users 102 | LEFT OUTER JOIN profiles ON profiles.user_id = users.id 103 | WHERE ((users.email = 'chris@ozmm.org' AND profiles.name = 'chris wanstrath'))" 104 | 105 | 106 | h2. Comparisons - select { |u| u.age > 21 } 107 | 108 | 109 | User.select { |m| m.age > 21 } 110 | "SELECT * FROM users WHERE users.age > 21" 111 | 112 | User.select { |m| m.age < 21 }.to_s 113 | "SELECT * FROM users WHERE users.age < 21" 114 | 115 | User.select { |m| [1, 2, 3, 4].include? m.id } 116 | "SELECT * FROM users WHERE users.id IN (1, 2, 3, 4)" 117 | 118 | 119 | h2. LIKE and REGEXP (RLIKE) - select { |m| m.name =~ 'chris' } 120 | 121 | 122 | User.select { |m| m.name =~ 'chris' } 123 | "SELECT * FROM users WHERE users.name LIKE 'chris'" 124 | 125 | User.select { |m| m.name =~ 'chri%' } 126 | "SELECT * FROM users WHERE users.name LIKE 'chri%'" 127 | 128 | User.select { |m| m.name !~ 'chris' } 129 | "SELECT * FROM users WHERE users.name NOT LIKE 'chris'" 130 | 131 | User.select { |m| !(m.name =~ 'chris') } 132 | "SELECT * FROM users WHERE users.name NOT LIKE 'chris'" 133 | 134 | User.select { |m| m.name =~ /chris/ } 135 | "SELECT * FROM users WHERE users.name REGEXP 'chris'" 136 | 137 | 138 | h2. #detect 139 | 140 | 141 | User.detect { |m| m.name == 'chris' } 142 | "SELECT * FROM users WHERE users.name = 'chris' LIMIT 1" 143 | 144 | 145 | h2. LIMITs - first, first(x), [offset, limit], [range], slice 146 | 147 | 148 | User.select { |m| m.name == 'jon' }.first 149 | "SELECT * FROM users WHERE users.name = 'jon' LIMIT 1" 150 | 151 | User.select { |m| m.name == 'jon' }.first(5) 152 | "SELECT * FROM users WHERE users.name = 'jon' LIMIT 5" 153 | 154 | User.select { |m| m.name == 'jon' }[10, 20] 155 | "SELECT * FROM users WHERE users.name = 'jon' LIMIT 10, 20" 156 | 157 | User.select { |m| m.name == 'jon' }[10..20] 158 | "SELECT * FROM users WHERE users.name = 'jon' LIMIT 10, 10" 159 | 160 | 161 | h2. ORDER - sort_by { |u| u.field } 162 | 163 | 164 | User.select { |m| m.name == 'jon' }.sort_by { |m| m.name } 165 | "SELECT * FROM users WHERE users.name = 'jon' ORDER BY users.name" 166 | 167 | User.select { |m| m.name == 'jon' }.sort_by { |m| [ m.name, m.age ] } 168 | "SELECT * FROM users WHERE users.name = 'jon' ORDER BY users.name, users.age" 169 | 170 | User.select { |m| m.name == 'jon' }.sort_by { |m| [ m.name, -m.age ] } 171 | "SELECT * FROM users WHERE users.name = 'jon' 172 | ORDER BY users.name, users.age DESC" 173 | 174 | User.select { |m| m.name == 'jon' }.sort_by { |m| [ -m.name, -m.age ] } 175 | "SELECT * FROM users WHERE users.name = 'jon' 176 | ORDER BY users.name DESC, users.age DESC" 177 | 178 | User.select { |m| m.name == 'jon' }.sort_by { |m| -m.age } 179 | "SELECT * FROM users WHERE users.name = 'jon' ORDER BY users.age DESC" 180 | 181 | User.select { |m| m.name == 'jon' }.sort_by { |m| -m.profiles.title } 182 | "SELECT users.id AS t0_r0, ... FROM users 183 | LEFT OUTER JOIN profiles ON profiles.user_id = users.id 184 | WHERE (users.name = 'jon') ORDER BY profiles.title DESC" 185 | 186 | User.select { |m| m.name == 'jon' }.sort_by { rand } 187 | "SELECT * FROM users WHERE users.name = 'jon' ORDER BY RAND()" 188 | 189 | 190 | h2. COUNT - select { |u| u.name == 'jon' }.size 191 | 192 | 193 | User.select { |m| m.name == 'jon' }.size 194 | "SELECT count(*) AS count_all FROM users WHERE (users.name = 'jon')" 195 | 196 | >> User.select { |m| m.name == 'jon' }.size 197 | => 21 198 | 199 | 200 | h2. Other Enumerables 201 | 202 | These methods perform COUNT() operations rather than loading your array into memory. They're all 203 | kickers. 204 | 205 | 206 | User.any? { |m| m.name == 'jon' } 207 | User.all? { |m| m.name == 'jon' } 208 | User.select { |m| m.name == 'jon' }.empty? 209 | 210 | 211 | h2. More Sugar 212 | 213 | The @downcase@ and @upcase@ methods will map to LOWER() and UPPER(), respectively. 214 | 215 | 216 | >> User.select { |m| m.name.downcase =~ 'jon%' }.to_s 217 | => "SELECT * FROM users WHERE LOWER(users.name) LIKE 'jon%'" 218 | 219 | 220 | h2. Quoting 221 | 222 | Columns and values will be quoted using ActiveRecord's quote_column_name and quote methods, if 223 | possible. 224 | 225 | h2. SELECT * FROM bugs 226 | 227 | Found a bug? Sweet. Add it at "the Lighthouse":http://err.lighthouseapp.com/projects/466-plugins/tickets/new. 228 | 229 | More information on Ambition: 230 | 231 | * "http://ambition.rubyforge.org":http://ambition.rubyforge.org 232 | * "http://groups.google.com/group/ambition-rb/":http://groups.google.com/group/ambition-rb/ 233 | 234 | - Chris Wanstrath [ chris@ozmm.org ] 235 | -------------------------------------------------------------------------------- /site/src/api.textile: -------------------------------------------------------------------------------- 1 | h2. Ambition::API 2 | 3 | Your data store target (e.g. @ActiveRecord@) is extended or injected with this 4 | module. 5 | 6 | h3. Processors 7 | 8 | These methods do not fire off a query and can be chained. They return a @Context@ 9 | object which can be inspected or kicked. 10 | 11 | * @select@ 12 | * @sort_by@ 13 | * @slice@ 14 | * @first@(length) 15 | 16 | h3. Kickers 17 | 18 | Kickers cause a query to execute. All method calls on a context are accumulated 19 | until kicked, at which point they're turned into a query and run. 20 | 21 | * @entries@ 22 | * @to_a@ 23 | * @detect@ 24 | * @first@ (no arguments) 25 | * @size@ 26 | 27 | h3. Custom Enumerables 28 | 29 | These @Enumerable@ methods are written special for Ambition. Other @Enumerable@ 30 | methods, like @each_with_index@, should work out of the box as they mostly wrap 31 | @each@. 32 | 33 | * @each@ 34 | * @any?@ 35 | * @all?@ 36 | * @empty?@ 37 | 38 | h2. Translators and Query 39 | 40 | Methods available to translator and Query instance methods: 41 | 42 | * @owner@ - constant, adapter target 43 | * @clauses@ - hash, keyed by translator name 44 | * @stash@ - hash, arbitrary 45 | 46 | h2. Ambition::Adapters::YourAdapter::Select 47 | 48 | All translators are instantiated and have access to @owner@ and @stash@. They should not 49 | touch @clauses@ directly. 50 | 51 | * @call@ - passed a symbol. e.g. @call(:name)@ 52 | * @chained_call@ - passed an array of symbols. e.g. @call(:name, :downcase)@ 53 | * @include?@ - passed the array it's called on and the argument. 54 | 55 | The following methods are passed the left and right side of the expression they represent. 56 | 57 | * @both@ - && 58 | * @either@ - || 59 | * @not_equal@ - != 60 | * @not_regexp@ - !~ 61 | * @==@ 62 | * @=~@ 63 | * @<@ 64 | * @<=@ 65 | * @>@ 66 | * @>=@ 67 | 68 | h2. Ambition::Adapters::YourAdapter::Slice 69 | 70 | All translators are instantiated and have access to @owner@ and @stash@. They should not 71 | touch @clauses@ directly. 72 | 73 | The @Slice@ translator has only one method: @slice@(start, length) 74 | 75 | Some examples: 76 | 77 | * @first(5)@ becomes @slice(0, 5)@ 78 | * @first@ becomes @slice(0, 1)@ 79 | * @User[10, 20]@ becomes @slice(10, 20)@ 80 | * @User[10..20]@ becomes @slice(10, 10)@ 81 | 82 | h2. Ambition::Adapters::YourAdapter::Sort 83 | 84 | All translators are instantiated and have access to @owner@ and @stash@. They should not 85 | touch @clauses@ directly. 86 | 87 | * @sort_by@ - passed a symbol 88 | * @reverse_sort_by@ - passed a symbol 89 | * @chained_sort_by@ - passed an array of symbols 90 | * @chained_reverse_sort_by@ - passed an array of symbols 91 | * @to_proc@ - passed a symbol 92 | * @rand@ 93 | 94 | h2. Ambition::Adapters::YourAdapter::Query 95 | 96 | The Query is instantiated and has access to @owner@, @clauses@, and @stash@. It should 97 | use the information these methods provide to build its domain specific query. 98 | 99 | When any of the following methods are called on a @Context@ they are forwarded to the Query 100 | instance. 101 | 102 | * kick - the API's kickers call this 103 | * size 104 | * to_hash 105 | * to_s -------------------------------------------------------------------------------- /site/src/index.textile: -------------------------------------------------------------------------------- 1 | h2. Plain Jane Ruby 2 | 3 | Imagine if instead of writing SQL, you could write Ruby. Instead of writing LDAP, you 4 | could write Ruby. Instead of learning some esoteric API, you just stick to one you know 5 | and love. 6 | 7 | Imagine *A*mbition. 8 | 9 | 10 | >> LDAP::User.select { |m| m.name == 'jon' && m.age == 21 }.to_s 11 | => "(&(name=jon)(age=21))" 12 | 13 | >> SQL::User.select { |m| m.name == 'jon' && m.age == 21 }.to_s 14 | => "SELECT * FROM users WHERE users.name = 'jon' AND users.age = 21" 15 | 16 | 17 | It "started":http://errtheblog.com/post/10722 with SQL, but it's become so much more. 18 | 19 | h2. The Elevator Pitch 20 | 21 | Ambition is a framework for writing adapters. Adapters are RubyGems which depend on the 22 | @ambition@ gem and are named something along the lines of @ambitious-activerecord@. They typically 23 | use Ambition to turn plain jane Ruby into some sort of domain specific query which can be executed. 24 | 25 | Anyone can write and release an adapter. This site describes how to write adapters using 26 | Ambition and also hosts a few. 27 | 28 | h2. The Community 29 | 30 | We've got a hoppin' community over at the "Google Group":http://groups.google.com/group/ambition-rb. Feel 31 | free to jump in with any questions, ideas, or top 5 favorite bands discussions. 32 | 33 | The initial blog posts were "this":http://errtheblog.com/post/10722 and "that":http://errtheblog.com/post/11998. 34 | Most of what they describe has become the @ambitious-activerecord@ gem. 35 | 36 | h2. Ambitious Adapters 37 | 38 | %INCLUDE _adapters.textile% 39 | 40 | For information on authoring adapters, hit up the "adapters":adapters.html page. 41 | 42 | h2. Development 43 | 44 | Development discussion happens in the "Google Group":http://groups.google.com/group/ambition-rb. 45 | Stay up to date by following Chris' git repository: 46 | 47 | @git clone git://github.com/defunkt/ambition@ 48 | -------------------------------------------------------------------------------- /site/src/layout.textile: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | Ruby's Ambition 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 23 | 24 |
25 | %CONTENT% 26 |
27 | 28 | 33 |
34 | 35 | 36 | -------------------------------------------------------------------------------- /site/src/static/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defunkt/ambition/b24b87b2d11d75fdb35f63ac9cfc0977cb1b9c39/site/src/static/bg.png -------------------------------------------------------------------------------- /site/src/static/code_highlighter.js: -------------------------------------------------------------------------------- 1 | /* Unobtrustive Code Highlighter By Dan Webb 11/2005 2 | Version: 0.4 3 | 4 | Usage: 5 | Add a script tag for this script and any stylesets you need to use 6 | to the page in question, add correct class names to CODE elements, 7 | define CSS styles for elements. That's it! 8 | 9 | Known to work on: 10 | IE 5.5+ PC 11 | Firefox/Mozilla PC/Mac 12 | Opera 7.23 + PC 13 | Safari 2 14 | 15 | Known to degrade gracefully on: 16 | IE5.0 PC 17 | 18 | Note: IE5.0 fails due to the use of lookahead in some stylesets. To avoid script errors 19 | in older browsers use expressions that use lookahead in string format when defining stylesets. 20 | 21 | This script is inspired by star-light by entirely cunning Dean Edwards 22 | http://dean.edwards.name/star-light/. 23 | */ 24 | 25 | // replace callback support for safari. 26 | if ("a".replace(/a/, function() {return "b"}) != "b") (function(){ 27 | var default_replace = String.prototype.replace; 28 | String.prototype.replace = function(search,replace){ 29 | // replace is not function 30 | if(typeof replace != "function"){ 31 | return default_replace.apply(this,arguments) 32 | } 33 | var str = "" + this; 34 | var callback = replace; 35 | // search string is not RegExp 36 | if(!(search instanceof RegExp)){ 37 | var idx = str.indexOf(search); 38 | return ( 39 | idx == -1 ? str : 40 | default_replace.apply(str,[search,callback(search, idx, str)]) 41 | ) 42 | } 43 | var reg = search; 44 | var result = []; 45 | var lastidx = reg.lastIndex; 46 | var re; 47 | while((re = reg.exec(str)) != null){ 48 | var idx = re.index; 49 | var args = re.concat(idx, str); 50 | result.push( 51 | str.slice(lastidx,idx), 52 | callback.apply(null,args).toString() 53 | ); 54 | if(!reg.global){ 55 | lastidx += RegExp.lastMatch.length; 56 | break 57 | }else{ 58 | lastidx = reg.lastIndex; 59 | } 60 | } 61 | result.push(str.slice(lastidx)); 62 | return result.join("") 63 | } 64 | })(); 65 | 66 | var CodeHighlighter = { styleSets : new Array }; 67 | 68 | CodeHighlighter.addStyle = function(name, rules) { 69 | // using push test to disallow older browsers from adding styleSets 70 | if ([].push) this.styleSets.push({ 71 | name : name, 72 | rules : rules, 73 | ignoreCase : arguments[2] || false 74 | }) 75 | 76 | function setEvent() { 77 | // set highlighter to run on load (use LowPro if present) 78 | if (typeof Event != 'undefined' && typeof Event.onReady == 'function') 79 | return Event.onReady(CodeHighlighter.init.bind(CodeHighlighter)); 80 | 81 | var old = window.onload; 82 | 83 | if (typeof window.onload != 'function') { 84 | window.onload = function() { CodeHighlighter.init() }; 85 | } else { 86 | window.onload = function() { 87 | old(); 88 | CodeHighlighter.init(); 89 | } 90 | } 91 | } 92 | 93 | // only set the event when the first style is added 94 | if (this.styleSets.length==1) setEvent(); 95 | } 96 | 97 | CodeHighlighter.init = function() { 98 | if (!document.getElementsByTagName) return; 99 | if ("a".replace(/a/, function() {return "b"}) != "b") return; // throw out Safari versions that don't support replace function 100 | // throw out older browsers 101 | 102 | var codeEls = document.getElementsByTagName("PRE"); 103 | // collect array of all pre elements 104 | codeEls.filter = function(f) { 105 | var a = new Array; 106 | for (var i = 0; i < this.length; i++) if (f(this[i])) a[a.length] = this[i]; 107 | return a; 108 | } 109 | 110 | var rules = new Array; 111 | rules.toString = function() { 112 | // joins regexes into one big parallel regex 113 | var exps = new Array; 114 | for (var i = 0; i < this.length; i++) exps.push(this[i].exp); 115 | return exps.join("|"); 116 | } 117 | 118 | function addRule(className, rule) { 119 | // add a replace rule 120 | var exp = (typeof rule.exp != "string")?String(rule.exp).substr(1, String(rule.exp).length-2):rule.exp; 121 | // converts regex rules to strings and chops of the slashes 122 | rules.push({ 123 | className : className, 124 | exp : "(" + exp + ")", 125 | length : (exp.match(/(^|[^\\])\([^?]/g) || "").length + 1, // number of subexps in rule 126 | replacement : rule.replacement || null 127 | }); 128 | } 129 | 130 | function parse(text, ignoreCase) { 131 | // main text parsing and replacement 132 | return text.replace(new RegExp(rules, (ignoreCase)?"gi":"g"), function() { 133 | var i = 0, j = 1, rule; 134 | while (rule = rules[i++]) { 135 | if (arguments[j]) { 136 | // if no custom replacement defined do the simple replacement 137 | if (!rule.replacement) return "" + arguments[0] + ""; 138 | else { 139 | // replace $0 with the className then do normal replaces 140 | var str = rule.replacement.replace("$0", rule.className); 141 | for (var k = 1; k <= rule.length - 1; k++) str = str.replace("$" + k, arguments[j + k]); 142 | return str; 143 | } 144 | } else j+= rule.length; 145 | } 146 | }); 147 | } 148 | 149 | function highlightCode(styleSet) { 150 | // clear rules array 151 | var parsed, clsRx = new RegExp("(\\s|^)" + styleSet.name + "(\\s|$)"); 152 | rules.length = 0; 153 | 154 | // get stylable elements by filtering out all code elements without the correct className 155 | var stylableEls = codeEls.filter(function(item) { return clsRx.test(item.className) }); 156 | 157 | // add style rules to parser 158 | for (var className in styleSet.rules) addRule(className, styleSet.rules[className]); 159 | 160 | 161 | // replace for all elements 162 | for (var i = 0; i < stylableEls.length; i++) { 163 | // EVIL hack to fix IE whitespace badness if it's inside a
164 | 			if (/MSIE/.test(navigator.appVersion) && stylableEls[i].parentNode.nodeName == 'PRE') {
165 | 				stylableEls[i] = stylableEls[i].parentNode;
166 | 				
167 | 				parsed = stylableEls[i].innerHTML.replace(/(]*>)([^<]*)<\/code>/i, function() {
168 | 					return arguments[1] + parse(arguments[2], styleSet.ignoreCase) + ""
169 | 				});
170 | 				parsed = parsed.replace(/\n( *)/g, function() { 
171 | 					var spaces = "";
172 | 					for (var i = 0; i < arguments[1].length; i++) spaces+= " ";
173 | 					return "\n" + spaces;  
174 | 				});
175 | 				parsed = parsed.replace(/\t/g, "    ");
176 | 				parsed = parsed.replace(/\n(<\/\w+>)?/g, "
$1").replace(/
[\n\r\s]*
/g, "


"); 177 | 178 | } else parsed = parse(stylableEls[i].innerHTML, styleSet.ignoreCase); 179 | 180 | stylableEls[i].innerHTML = parsed; 181 | } 182 | } 183 | 184 | // run highlighter on all stylesets 185 | for (var i=0; i < this.styleSets.length; i++) { 186 | highlightCode(this.styleSets[i]); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /site/src/static/css.js: -------------------------------------------------------------------------------- 1 | CodeHighlighter.addStyle("css", { 2 | comment : { 3 | exp : /\/\*[^*]*\*+([^\/][^*]*\*+)*\// 4 | }, 5 | keywords : { 6 | exp : /@\w[\w\s]*/ 7 | }, 8 | selectors : { 9 | exp : "([\\w-:\\[.#][^{};>]*)(?={)" 10 | }, 11 | properties : { 12 | exp : "([\\w-]+)(?=\\s*:)" 13 | }, 14 | units : { 15 | exp : /([0-9])(em|en|px|%|pt)\b/, 16 | replacement : "$1$2" 17 | }, 18 | urls : { 19 | exp : /url\([^\)]*\)/ 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /site/src/static/html.js: -------------------------------------------------------------------------------- 1 | CodeHighlighter.addStyle("html", { 2 | comment : { 3 | exp: /<!\s*(--([^-]|[\r\n]|-[^-])*--\s*)>/ 4 | }, 5 | tag : { 6 | exp: /(<\/?)([a-zA-Z]+\s?)/, 7 | replacement: "$1$2" 8 | }, 9 | string : { 10 | exp : /'[^']*'|"[^"]*"/ 11 | }, 12 | attribute : { 13 | exp: /\b([a-zA-Z-:]+)(=)/, 14 | replacement: "$1$2" 15 | }, 16 | doctype : { 17 | exp: /<!DOCTYPE([^&]|&[^g]|&g[^t])*>/ 18 | } 19 | }); -------------------------------------------------------------------------------- /site/src/static/hubris.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body { 7 | background: url(/static/bg.png) repeat; 8 | color: #333; 9 | font-family: serif; 10 | } 11 | 12 | #main { 13 | background: white; 14 | width: 700px; 15 | margin: 0 auto; 16 | border-right: 2px solid black; 17 | border-bottom: 2px solid black; 18 | } 19 | 20 | #header { 21 | border-bottom: 1px solid #aaa; 22 | background: #eee; 23 | padding-left: 10px; 24 | margin-bottom: 10px; 25 | } 26 | 27 | #header h1 { 28 | font-family: georgia; 29 | color: #aaa; 30 | letter-spacing: -3px; 31 | font-size: 3em; 32 | text-shadow: #0c0c0c 2px 2px 1px; 33 | } 34 | 35 | .a { 36 | color: red; 37 | } 38 | 39 | #header h1 .a { 40 | font-size: 1.4em; 41 | } 42 | 43 | #nav { 44 | float: right; 45 | padding: 10px; 46 | margin-top: -35px; 47 | font-family: sans-serif; 48 | font-size: 0.9em; 49 | } 50 | 51 | #nav a { 52 | text-decoration: none; 53 | } 54 | 55 | #nav a:hover { 56 | text-decoration: underline; 57 | } 58 | 59 | #body { 60 | padding: 0 20px 0 10px; 61 | } 62 | 63 | h2 { 64 | border-bottom: 1px dotted #aaa; 65 | margin-bottom: 10px; 66 | margin-top: 15px; 67 | } 68 | 69 | ul, p { 70 | padding: 0 0 10px 10px; 71 | line-height: 17px; 72 | font-family: verdana, arial, sans-serif; 73 | font-size: 0.7em; 74 | } 75 | 76 | a { 77 | color: #333; 78 | } 79 | 80 | a:hover { 81 | text-decoration: none; 82 | } 83 | 84 | ul { 85 | margin-left: 20px; 86 | } 87 | 88 | code { 89 | font-size: 1.3em; 90 | background: #eee; 91 | } 92 | 93 | pre { 94 | margin: 0 18px 10px 10px; 95 | padding: 10px; 96 | border: 1px solid #aaa; 97 | background: #eee; 98 | } 99 | 100 | #footer { 101 | font-family: helvetica; 102 | font-size:0.9em; 103 | border-top: 1px solid #aaa; 104 | background: #eee; 105 | padding: 10px; 106 | text-align: center; 107 | margin-top: 15px; 108 | } 109 | 110 | .ruby .string { 111 | color: #ff0000; 112 | } 113 | .ruby .brackets { 114 | color: #777; 115 | } 116 | 117 | .ruby .regex { 118 | color: #37ffff; 119 | } 120 | 121 | .normal {} 122 | .comment { color: #9900FF; } 123 | .keywords, .expression { color: #C71585; font-weight: bold; } 124 | .method { color: #077; } 125 | .class { color: #074; } 126 | .module { color: #050; } 127 | .punct { color: #447; font-weight: bold; } 128 | .symbol { color: #099; } 129 | .string { color: #6600CC; background: #FFE; } 130 | .char { color: #F07; } 131 | .ident { color: #004; } 132 | .constant { color: #07F; } 133 | .regex { color: #B66; background: #FEF; } 134 | .number { color: blue; } 135 | .attribute { color: #7BB; } 136 | .global { color: #7FB; } 137 | .expr { color: #227; } 138 | .escape { color: #277; } 139 | 140 | -------------------------------------------------------------------------------- /site/src/static/javascript.js: -------------------------------------------------------------------------------- 1 | CodeHighlighter.addStyle("javascript",{ 2 | comment : { 3 | exp : /(\/\/[^\n]*(\n|$))|(\/\*[^*]*\*+([^\/][^*]*\*+)*\/)/ 4 | }, 5 | brackets : { 6 | exp : /\(|\)/ 7 | }, 8 | string : { 9 | exp : /'[^']*'|"[^"]*"/ 10 | }, 11 | keywords : { 12 | exp : /\b(arguments|break|case|continue|default|delete|do|else|false|for|function|if|in|instanceof|new|null|return|switch|this|true|typeof|var|void|while|with)\b/ 13 | }, 14 | global : { 15 | exp : /\b(toString|valueOf|window|element|prototype|constructor|document|escape|unescape|parseInt|parseFloat|setTimeout|clearTimeout|setInterval|clearInterval|NaN|isNaN|Infinity)\b/ 16 | } 17 | }); -------------------------------------------------------------------------------- /site/src/static/ruby.js: -------------------------------------------------------------------------------- 1 | CodeHighlighter.addStyle("ruby",{ 2 | comment : { 3 | exp : /(#[^\n]+)|(#\s*\n)/ 4 | }, 5 | brackets : { 6 | exp : /\(|\)/ 7 | }, 8 | string : { 9 | exp : /'[^']*'|"[^"]*"/ 10 | }, 11 | keywords : { 12 | exp : /\b(extend|def|module|class|require)\b/ 13 | }, 14 | expression : { 15 | exp : /\b(do|end|if|yield|then|else|for|trap|exit|until|unless|while|elsif|case|when|break|retry|redo|rescue|raise)\b|\bdefined\?/ }, 16 | /* Added by Shelly Fisher (shelly@agileevolved.com) */ 17 | symbol : { 18 | exp : /([^:])(:[A-Za-z0-9_!?]+)/ 19 | }, 20 | constant : { 21 | exp : /[A-Z]{1}\w+|\bself\b/ 22 | }, 23 | regex : { 24 | exp : /\/.+\// 25 | }, 26 | number : { 27 | exp : /\d/ 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /test/adapters/exemplar/association_test.rb: -------------------------------------------------------------------------------- 1 | # Analog of the join_test.rb in the ActiveRecord adapter. 2 | # Not all adapters will need these behaviors. E.g. there are no joins in LDAP. 3 | # ActiveLdap has associations, but doesn't try to fake joins. 4 | # CouchDb doesn't so much have joins as it has the capacity for creative 5 | # use of its views. 6 | 7 | require File.dirname(__FILE__) + '/helper' 8 | 9 | context "Object associations" do 10 | xspecify "simple ==" do 11 | result = User.select { |m| m.account.email == 'chris@ozmm.org' } 12 | end 13 | 14 | xspecify "simple mixed == on object and an association" do 15 | result = User.select { |m| m.name == 'chris' && m.account.email == 'chris@ozmm.org' } 16 | end 17 | 18 | xspecify "multiple associations" do 19 | result = User.select { |m| m.ideas.title == 'New Freezer' || m.invites.email == 'pj@hyett.com' } 20 | end 21 | 22 | xspecify "non-existant associations" do 23 | result = User.select { |m| m.liquor.brand == 'Jack' } 24 | should.raise { result.to_hash } 25 | end 26 | 27 | xspecify "simple order by association attribute" do 28 | result = User.sort_by { |m| m.ideas.title } 29 | end 30 | 31 | xspecify "in a more complex order" do 32 | result = User.sort_by { |m| [ m.ideas.title, -m.invites.email ] } 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/adapters/exemplar/count_test.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defunkt/ambition/b24b87b2d11d75fdb35f63ac9cfc0977cb1b9c39/test/adapters/exemplar/count_test.rb -------------------------------------------------------------------------------- /test/adapters/exemplar/detect_test.rb: -------------------------------------------------------------------------------- 1 | context "Exemplar Adapter :: Detect" do 2 | xspecify "simple ==" do 3 | User.detect { |m| m.name == 'chris' } 4 | end 5 | 6 | xspecify "nothing found" do 7 | User.detect { |m| m.name == 'chris' }.should.be.nil 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/adapters/exemplar/enumerable_test.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defunkt/ambition/b24b87b2d11d75fdb35f63ac9cfc0977cb1b9c39/test/adapters/exemplar/enumerable_test.rb -------------------------------------------------------------------------------- /test/adapters/exemplar/helper.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../helper' 2 | 3 | # require 'ambition/adapters/exemplar' -------------------------------------------------------------------------------- /test/adapters/exemplar/index_operator.rb: -------------------------------------------------------------------------------- 1 | xcontext "Exemplar Adapter :: [] operator" do 2 | specify "finds a single row" do 3 | User.expects(:find).with(1) 4 | User[1] 5 | end 6 | end -------------------------------------------------------------------------------- /test/adapters/exemplar/reject_test.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defunkt/ambition/b24b87b2d11d75fdb35f63ac9cfc0977cb1b9c39/test/adapters/exemplar/reject_test.rb -------------------------------------------------------------------------------- /test/adapters/exemplar/select_test.rb: -------------------------------------------------------------------------------- 1 | context "Exemplar Adapter :: Select" do 2 | xspecify "simple ==" do 3 | translator = User.select { |m| m.name == 'jon' } 4 | translator.to_s.should == %Q(foo) 5 | end 6 | 7 | xspecify "simple !=" do 8 | translator = User.select { |m| m.name != 'jon' } 9 | translator.to_s.should == %Q(foo) 10 | end 11 | 12 | xspecify "simple == && ==" do 13 | translator = User.select { |m| m.name == 'jon' && m.age == 21 } 14 | translator.to_s.should == %Q(foo) 15 | end 16 | 17 | xspecify "simple == || ==" do 18 | translator = User.select { |m| m.name == 'jon' || m.age == 21 } 19 | translator.to_s.should == %Q(foo) 20 | end 21 | 22 | xspecify "mixed && and ||" do 23 | translator = User.select { |m| m.name == 'jon' || m.age == 21 && m.password == 'pass' } 24 | translator.to_s.should == %Q(foo) 25 | end 26 | 27 | xspecify "grouped && and ||" do 28 | translator = User.select { |m| (m.name == 'jon' || m.name == 'rick') && m.age == 21 } 29 | translator.to_s.should == %Q(foo) 30 | end 31 | 32 | xspecify "simple >/<" do 33 | translator = User.select { |m| m.age > 21 } 34 | translator.to_s.should == %Q(foo) 35 | 36 | translator = User.select { |m| m.age >= 21 } 37 | translator.to_s.should == %Q(foo) 38 | 39 | translator = User.select { |m| m.age < 21 } 40 | translator.to_s.should == %Q(foo) 41 | end 42 | 43 | xspecify "array.include? item" do 44 | translator = User.select { |m| [1, 2, 3, 4].include? m.id } 45 | translator.to_s.should == %Q(foo) 46 | end 47 | 48 | xspecify "variabled array.include? item" do 49 | array = [1, 2, 3, 4] 50 | translator = User.select { |m| array.include? m.id } 51 | translator.to_s.should == %Q(foo) 52 | end 53 | 54 | xspecify "simple == with variables" do 55 | me = 'chris' 56 | translator = User.select { |m| m.name == me } 57 | translator.to_s.should == %Q(foo) 58 | end 59 | 60 | xspecify "simple == with method arguments" do 61 | def test_it(name) 62 | translator = User.select { |m| m.name == name } 63 | translator.to_s.should == %Q(foo) 64 | end 65 | 66 | test_it('chris') 67 | end 68 | 69 | xspecify "simple == with instance variables" do 70 | @me = 'chris' 71 | translator = User.select { |m| m.name == @me } 72 | translator.to_s.should == %Q(foo) 73 | end 74 | 75 | xspecify "simple == with instance variable method call" do 76 | require 'ostruct' 77 | @person = OpenStruct.new(:name => 'chris') 78 | 79 | translator = User.select { |m| m.name == @person.name } 80 | translator.to_s.should == %Q(foo) 81 | end 82 | 83 | xspecify "simple == with global variables" do 84 | $my_name = 'boston' 85 | translator = User.select { |m| m.name == $my_name } 86 | translator.to_s.should == %Q(foo) 87 | end 88 | 89 | xspecify "simple == with method call" do 90 | def band 91 | 'skinny puppy' 92 | end 93 | 94 | translator = User.select { |m| m.name == band } 95 | translator.to_s.should == %Q(foo) 96 | end 97 | 98 | xspecify "simple =~ with string" do 99 | translator = User.select { |m| m.name =~ 'chris' } 100 | translator.to_s.should == %Q(foo) 101 | 102 | translator = User.select { |m| m.name =~ 'chri%' } 103 | translator.to_s.should == %Q(foo) 104 | end 105 | 106 | xspecify "simple !~ with string" do 107 | translator = User.select { |m| m.name !~ 'chris' } 108 | translator.to_s.should == %Q(foo) 109 | 110 | translator = User.select { |m| !(m.name =~ 'chris') } 111 | translator.to_s.should == %Q(foo) 112 | end 113 | 114 | xspecify "simple =~ with regexp" do 115 | translator = User.select { |m| m.name =~ /chris/ } 116 | translator.to_s.should == %Q(foo) 117 | end 118 | 119 | xspecify "simple =~ with regexp flags" do 120 | translator = User.select { |m| m.name =~ /chris/i } 121 | translator.to_s.should == %Q(foo) 122 | end 123 | 124 | xspecify "simple downcase" do 125 | translator = User.select { |m| m.name.downcase =~ 'chris%' } 126 | translator.to_s.should == %Q(foo) 127 | end 128 | 129 | xspecify "simple upcase" do 130 | translator = User.select { |m| m.name.upcase =~ 'chris%' } 131 | translator.to_s.should == %Q(foo) 132 | end 133 | 134 | xspecify "undefined equality symbol" do 135 | should.raise { User.select { |m| m.name =* /chris/ } } 136 | end 137 | 138 | xspecify "block variable / assigning variable conflict" do 139 | m = User.select { |m| m.name == 'chris' } 140 | m.should == %Q(foo) 141 | end 142 | 143 | xspecify "simple == with inline ruby" do 144 | translator = User.select { |m| m.created_at == 2.days.ago(:db) } 145 | translator.to_s.should == %Q(foo) 146 | end 147 | 148 | xspecify "inspect" do 149 | User.select { |u| u.name }.inspect.should.match %r(call #to_s or #to_hash) 150 | end 151 | end -------------------------------------------------------------------------------- /test/adapters/exemplar/slice_test.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defunkt/ambition/b24b87b2d11d75fdb35f63ac9cfc0977cb1b9c39/test/adapters/exemplar/slice_test.rb -------------------------------------------------------------------------------- /test/adapters/exemplar/sort_test.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defunkt/ambition/b24b87b2d11d75fdb35f63ac9cfc0977cb1b9c39/test/adapters/exemplar/sort_test.rb -------------------------------------------------------------------------------- /test/debug: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb' 3 | libs = [] 4 | dirname = File.dirname(__FILE__) 5 | 6 | libs << 'irb/completion' 7 | libs << File.join(dirname, '..', 'lib', 'ambition') 8 | 9 | exec "#{irb} -Ilib #{libs.map{|l| " -r #{l}" }.join} --simple-prompt" 10 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | %w( rubygems test/spec mocha redgreen English ).each { |f| require f } 2 | 3 | $LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib' 4 | require 'ambition' 5 | --------------------------------------------------------------------------------