├── .gitignore ├── test ├── adapters │ └── exemplar │ │ ├── count_test.rb │ │ ├── reject_test.rb │ │ ├── slice_test.rb │ │ ├── sort_test.rb │ │ ├── enumerable_test.rb │ │ ├── helper.rb │ │ ├── index_operator.rb │ │ ├── detect_test.rb │ │ ├── association_test.rb │ │ └── select_test.rb ├── helper.rb └── debug ├── app_generators └── ambition_adapter │ ├── USAGE │ ├── templates │ ├── README │ ├── test │ │ ├── helper.rb.erb │ │ ├── slice_test.rb.erb │ │ ├── sort_test.rb.erb │ │ └── select_test.rb.erb │ ├── lib │ │ ├── adapter │ │ │ ├── base.rb.erb │ │ │ ├── slice.rb.erb │ │ │ ├── sort.rb.erb │ │ │ ├── query.rb.erb │ │ │ └── select.rb.erb │ │ └── init.rb.erb │ ├── Rakefile │ └── LICENSE │ └── ambition_adapter_generator.rb ├── site ├── src │ ├── static │ │ ├── bg.png │ │ ├── css.js │ │ ├── html.js │ │ ├── javascript.js │ │ ├── ruby.js │ │ ├── hubris.css │ │ └── code_highlighter.js │ ├── _adapters.textile │ ├── layout.textile │ ├── index.textile │ ├── api.textile │ ├── adapters.textile │ └── adapters │ │ └── activerecord.textile └── Rakefile ├── deps.rip ├── lib ├── ambition │ ├── enumerable.rb │ ├── core_ext.rb │ ├── processors │ │ ├── slice.rb │ │ ├── ruby.rb │ │ ├── sort.rb │ │ ├── select.rb │ │ └── base.rb │ ├── sexp_translator.rb │ ├── context.rb │ └── api.rb └── ambition.rb ├── bin └── ambition_adapter ├── README ├── LICENSE ├── Manifest ├── Rakefile └── ambition.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | pkg 2 | live 3 | -------------------------------------------------------------------------------- /test/adapters/exemplar/count_test.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/adapters/exemplar/reject_test.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/adapters/exemplar/slice_test.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/adapters/exemplar/sort_test.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/adapters/exemplar/enumerable_test.rb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app_generators/ambition_adapter/USAGE: -------------------------------------------------------------------------------- 1 | o. 2 | -------------------------------------------------------------------------------- /site/src/static/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defunkt/ambition/master/site/src/static/bg.png -------------------------------------------------------------------------------- /test/adapters/exemplar/helper.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../../helper' 2 | 3 | # require 'ambition/adapters/exemplar' -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | }); -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | }); -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /site/src/layout.textile: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 |
').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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | -------------------------------------------------------------------------------- /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
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/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 |
--------------------------------------------------------------------------------