├── .gitignore ├── Gemfile ├── LICENSE ├── README ├── Rakefile ├── bin └── cheat ├── cheat.gemspec ├── favicon.ico └── lib ├── ambition ├── LICENSE ├── README ├── Rakefile ├── init.rb ├── lib │ ├── ambition.rb │ ├── ambition │ │ ├── count.rb │ │ ├── enumerable.rb │ │ ├── limit.rb │ │ ├── order.rb │ │ ├── processor.rb │ │ ├── query.rb │ │ └── where.rb │ └── proc_to_ruby.rb └── test │ ├── chaining_test.rb │ ├── count_test.rb │ ├── enumerable_test.rb │ ├── helper.rb │ ├── join_test.rb │ ├── limit_test.rb │ ├── order_test.rb │ ├── types_test.rb │ └── where_test.rb ├── cheat.rb └── cheat ├── diffr.rb ├── responder.rb ├── rv_harness.rb ├── site.rb ├── version.rb └── wrap.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /pkg 3 | Gemfile.lock 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'rake' 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2006 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 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | == Cheat 2 | 3 | Cheat is a simple command line reference utility. Use it to retrieve handy 4 | information from the cheat sheet repository. 5 | 6 | $ cheat sheets 7 | $ cheat cheat 8 | $ cheat recent 9 | 10 | To freshen your local cache, supply the --new edit. 11 | 12 | $ cheat sheets --new 13 | 14 | To edit a cheat sheet, use the --edit switch. 15 | 16 | $ cheat markaby --edit 17 | 18 | To add a cheat sheet, use the --add switch. 19 | 20 | $ cheat readme --add 21 | 22 | To execute a sheet, use the --execute or -x switch. 23 | 24 | $ cheat rspec_rails_install_edge --execute 25 | $ cheat rspec_rails_install_edge --x 26 | 27 | Special Thanks To: 28 | - Evan Weaver 29 | - Kevin Marsh 30 | - Jeremy Apthorp 31 | 32 | The Cheat Sheet Repository: 33 | - http://cheat.errtheblog.com/ 34 | 35 | Enjoy. 36 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | -------------------------------------------------------------------------------- /bin/cheat: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | begin 4 | require 'cheat' 5 | rescue LoadError 6 | require 'rubygems' 7 | require 'cheat' 8 | end 9 | 10 | Cheat.sheets(ARGV) 11 | -------------------------------------------------------------------------------- /cheat.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'cheat/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.add_dependency 'pager', '~> 1.0' 8 | spec.add_development_dependency 'bundler', '~> 1.0' 9 | spec.name = 'cheat' 10 | spec.description = "cheat prints cheat sheets from cheat.errtheblog.com, a wiki-like repository of programming knowledge." 11 | spec.summary = "cheat prints cheat sheets from cheat.errtheblog.com" 12 | spec.authors = ["Chris Wanstrath", "Erik Michaels-Ober"] 13 | spec.email = ["chris@ozmm.org", "sferik@gmail.com"] 14 | spec.bindir = 'bin' 15 | spec.executables = %w(cheat) 16 | spec.files = %w(README LICENSE) 17 | spec.files += Dir.glob("bin/**/*") 18 | spec.files += Dir.glob("lib/**/*") 19 | spec.files += Dir.glob("man/**/*") 20 | spec.files += Dir.glob("test/**/*") 21 | spec.homepage = 'http://cheat.errtheblog.com' 22 | spec.licenses = ['MIT'] 23 | spec.require_paths = ['lib'] 24 | spec.required_rubygems_version = '>= 1.3.5' 25 | spec.version = Cheat::Version 26 | end 27 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/defunkt/cheat/e3ea552f0db5e6ae861fc712c105e6edab6c2003/favicon.ico -------------------------------------------------------------------------------- /lib/ambition/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 | -------------------------------------------------------------------------------- /lib/ambition/README: -------------------------------------------------------------------------------- 1 | == Ambitious SQL 2 | 3 | A simple experiment and even simpler ActiveRecord library. 4 | 5 | I could tell you all about how awesome the internals are, or 6 | how fun it was to write, or how it'll make you rich and famous, 7 | but instead I'm just going to show you some examples. 8 | 9 | == Examples 10 | 11 | Basically, you write your SQL in Ruby. No, not in Ruby. As Ruby. 12 | 13 | User.select { |u| u.city == 'San Francisco' }.each do |user| 14 | puts user.name 15 | end 16 | 17 | And that's it. 18 | 19 | The key is the +each+ method. You build up a +Query+ using +select+, +detect+, 20 | +limit+, and +sort_by+, then call +each+ on it. This'll run the query and enumerate 21 | through the results. 22 | 23 | Our +Query+ object has two useful methods: +to_sql+ and +to_hash+. With these, we can 24 | check out what exactly we're building. 25 | 26 | See, +to_sql+: 27 | >> User.select { |m| m.name == 'jon' }.to_sql 28 | => "SELECT * FROM users WHERE users.`name` = 'jon'" 29 | 30 | See, +to_hash+: 31 | >> User.select { |m| m.name == 'jon' }.to_hash 32 | => {:conditions=>"users.`name` = 'jon'"} 33 | 34 | == Limitations 35 | 36 | You can use variables, but any more complex Ruby (right now) won't work 37 | inside your blocks. Just do it outside the block and assign it to a variable, okay? 38 | 39 | Instead of: 40 | User.select { |m| m.date == 2.days.ago } 41 | 42 | Just do: 43 | date = 2.days.ago 44 | User.select { |m| m.date == date } 45 | 46 | Instance variables and globals work, too. 47 | 48 | == Equality -- select { |u| u.field == 'bob' } 49 | 50 | >> User.select { |m| m.name == 'jon' }.to_sql 51 | => "SELECT * FROM users WHERE users.`name` = 'jon'" 52 | 53 | >> User.select { |m| m.name != 'jon' }.to_sql 54 | => "SELECT * FROM users WHERE users.`name` <> 'jon'" 55 | 56 | >> User.select { |m| m.name == 'jon' && m.age == 21 }.to_sql 57 | => "SELECT * FROM users WHERE (users.`name` = 'jon' AND users.`age` = 21)" 58 | 59 | >> User.select { |m| m.name == 'jon' || m.age == 21 }.to_sql 60 | => "SELECT * FROM users WHERE (users.`name` = 'jon' OR users.`age` = 21)" 61 | 62 | >> User.select { |m| m.name == 'jon' || m.age == 21 && m.password == 'pass' }.to_sql 63 | => "SELECT * FROM users WHERE (users.`name` = 'jon' OR (users.`age` = 21 AND users.`password` = 'pass'))" 64 | 65 | >> User.select { |m| (m.name == 'jon' || m.name == 'rick') && m.age == 21 }.to_sql 66 | => "SELECT * FROM users WHERE ((users.`name` = 'jon' OR users.`name` = 'rick') AND users.`age` = 21)" 67 | 68 | == Comparisons -- select { |u| u.age > 21 } 69 | 70 | >> User.select { |m| m.age > 21 }.to_sql 71 | => "SELECT * FROM users WHERE users.`age` > 21" 72 | 73 | >> User.select { |m| m.age < 21 }.to_sql 74 | => "SELECT * FROM users WHERE users.`age` < 21" 75 | 76 | >> sql = User.select { |m| [1, 2, 3, 4].include? m.id }.to_sql 77 | => "SELECT * FROM users WHERE users.`id` IN (1, 2, 3, 4)" 78 | 79 | == LIKE and REGEXP (RLIKE) -- select { |m| m.name =~ 'chris' } 80 | 81 | >> User.select { |m| m.name =~ 'chris' }.to_sql 82 | => "SELECT * FROM users WHERE users.`name` LIKE 'chris'" 83 | 84 | >> User.select { |m| m.name =~ 'chri%' }.to_sql 85 | => "SELECT * FROM users WHERE users.`name` LIKE 'chri%'" 86 | 87 | >> User.select { |m| m.name !~ 'chris' }.to_sql 88 | => "SELECT * FROM users WHERE users.`name` NOT LIKE 'chris'" 89 | 90 | >> User.select { |m| !(m.name =~ 'chris') }.to_sql 91 | => "SELECT * FROM users WHERE users.`name` NOT LIKE 'chris'" 92 | 93 | >> User.select { |m| m.name =~ /chris/ }.to_sql 94 | => "SELECT * FROM users WHERE users.`name` REGEXP 'chris'" 95 | 96 | == #detect 97 | 98 | >> User.detect { |m| m.name == 'chris' }.to_sql 99 | => "SELECT * FROM users WHERE users.`name` = 'chris' LIMIT 1" 100 | 101 | == LIMITs -- first, first(x), [offset, limit] 102 | 103 | >> User.select { |m| m.name == 'jon' }.first.to_sql 104 | => "SELECT * FROM users WHERE users.`name` = 'jon' LIMIT 1" 105 | 106 | >> User.select { |m| m.name == 'jon' }.first(5).to_sql 107 | => "SELECT * FROM users WHERE users.`name` = 'jon' LIMIT 5" 108 | 109 | >> User.select { |m| m.name == 'jon' }[10, 20].to_sql 110 | => "SELECT * FROM users WHERE users.`name` = 'jon' LIMIT 10, 20" 111 | 112 | == ORDER -- sort_by { |u| u.field } 113 | 114 | >> User.select { |m| m.name == 'jon' }.sort_by { |m| m.name }.to_sql 115 | => "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY name" 116 | 117 | >> User.select { |m| m.name == 'jon' }.sort_by { |m| [ m.name, m.age ] }.to_sql 118 | => "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY name, age" 119 | 120 | >> User.select { |m| m.name == 'jon' }.sort_by { |m| [ m.name, -m.age ] }.to_sql 121 | => "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY name, age DESC" 122 | 123 | >> User.select { |m| m.name == 'jon' }.sort_by { |m| [ -m.name, -m.age ] }.to_sql 124 | => "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY name DESC, age DESC" 125 | 126 | >> User.select { |m| m.name == 'jon' }.sort_by { |m| -m.age }.to_sql 127 | => "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY age DESC" 128 | 129 | >> User.select { |m| m.name == 'jon' }.sort_by { rand }.to_sql 130 | => "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY RAND()" 131 | 132 | == COUNT -- select { |u| u.name == 'jon' }.size 133 | 134 | >> User.select { |m| m.name == 'jon' }.size 135 | => 21 136 | 137 | == SELECT * FROM bugs 138 | 139 | Found a bug? Sweet. Add it at the Lighthouse: http://err.lighthouseapp.com/projects/466-plugins/tickets/new 140 | 141 | Feature requests are welcome. Ideas for cross-table stuff and joins, especially. 142 | 143 | * Chris Wanstrath [ chris@ozmm.org ] 144 | -------------------------------------------------------------------------------- /lib/ambition/Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'rake/rdoctask' 4 | 5 | desc 'Default: run unit tests.' 6 | task :default => :test 7 | 8 | desc 'Test it!' 9 | Rake::TestTask.new(:test) do |t| 10 | t.pattern = 'test/**/*_test.rb' 11 | t.verbose = true 12 | end 13 | 14 | desc 'Generate RDoc documentation' 15 | Rake::RDocTask.new(:rdoc) do |rdoc| 16 | files = ['README', 'LICENSE', 'lib/**/*.rb'] 17 | rdoc.rdoc_files.add(files) 18 | rdoc.main = "README" # page to start on 19 | rdoc.title = "ambition" 20 | rdoc.template = File.exists?(t="/Users/chris/ruby/projects/err/rock/template.rb") ? t : "/var/www/rock/template.rb" 21 | rdoc.rdoc_dir = 'doc' # rdoc output folder 22 | rdoc.options << '--inline-source' 23 | end 24 | 25 | desc 'Generate coverage reports' 26 | task :rcov do 27 | `rcov -e gems test/*_test.rb` 28 | puts 'Generated coverage reports.' 29 | end 30 | -------------------------------------------------------------------------------- /lib/ambition/init.rb: -------------------------------------------------------------------------------- 1 | require 'ambition' 2 | -------------------------------------------------------------------------------- /lib/ambition/lib/ambition.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'proc_to_ruby' 3 | require 'ambition/processor' 4 | require 'ambition/query' 5 | require 'ambition/where' 6 | require 'ambition/order' 7 | require 'ambition/limit' 8 | require 'ambition/count' 9 | require 'ambition/enumerable' 10 | 11 | module Ambition 12 | include Where, Order, Limit, Enumerable, Count 13 | 14 | attr_accessor :query_context 15 | 16 | def query_context 17 | @query_context || Query.new(self) 18 | end 19 | end 20 | 21 | ActiveRecord::Base.extend Ambition 22 | -------------------------------------------------------------------------------- /lib/ambition/lib/ambition/count.rb: -------------------------------------------------------------------------------- 1 | module Ambition 2 | module Count 3 | def size 4 | count(query_context.to_hash) 5 | end 6 | alias_method :length, :size 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/ambition/lib/ambition/enumerable.rb: -------------------------------------------------------------------------------- 1 | module Ambition 2 | module Enumerable 3 | include ::Enumerable 4 | 5 | def each(&block) 6 | find(:all, query_context.to_hash).each(&block) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/ambition/lib/ambition/limit.rb: -------------------------------------------------------------------------------- 1 | module Ambition 2 | module Limit 3 | def first(limit = 1, offset = nil) 4 | query_context.add LimitProcessor.new(limit, offset) 5 | find(limit == 1 ? :first : :all, query_context.to_hash) 6 | end 7 | 8 | def [](offset, limit = nil) 9 | return first(offset, limit) if limit 10 | 11 | if offset.is_a? Range 12 | limit = offset.end 13 | limit -= 1 if offset.exclude_end? 14 | first(offset.first, limit - offset.first) 15 | else 16 | first(offset, 1) 17 | end 18 | end 19 | end 20 | 21 | class LimitProcessor 22 | def initialize(*args) 23 | @args = args 24 | end 25 | 26 | def key 27 | :limit 28 | end 29 | 30 | def to_s 31 | @args.compact * ', ' 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/ambition/lib/ambition/order.rb: -------------------------------------------------------------------------------- 1 | module Ambition 2 | module Order 3 | def sort_by(&block) 4 | query_context.add OrderProcessor.new(table_name, block) 5 | end 6 | end 7 | 8 | class OrderProcessor < Processor 9 | def initialize(table_name, block) 10 | super() 11 | @receiver = nil 12 | @table_name = table_name 13 | @block = block 14 | @key = :order 15 | end 16 | 17 | ## 18 | # Sexp Processing Methods 19 | def process_call(exp) 20 | receiver, method, other = *exp 21 | exp.clear 22 | 23 | translation(receiver, method, other) 24 | end 25 | 26 | def process_vcall(exp) 27 | if (method = exp.shift) == :rand 28 | 'RAND()' 29 | else 30 | raise "Not implemented: :vcall for #{method}" 31 | end 32 | end 33 | 34 | def process_masgn(exp) 35 | exp.clear 36 | '' 37 | end 38 | 39 | ## 40 | # Helpers! 41 | def translation(receiver, method, other) 42 | case method 43 | when :-@ 44 | "#{process(receiver)} DESC" 45 | when :__send__ 46 | "#{@table_name}.#{eval('to_s', @block)}" 47 | else 48 | "#{@table_name}.#{method}" 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/ambition/lib/ambition/processor.rb: -------------------------------------------------------------------------------- 1 | require 'active_record/connection_adapters/abstract/quoting' 2 | 3 | module Ambition 4 | class Processor < SexpProcessor 5 | include ActiveRecord::ConnectionAdapters::Quoting 6 | 7 | attr_reader :key, :join_string, :prefix 8 | 9 | def initialize 10 | super() 11 | @strict = false 12 | @expected = String 13 | @auto_shift_type = true 14 | @warn_on_default = false 15 | @default_method = :process_error 16 | end 17 | 18 | ## 19 | # Processing methods 20 | def process_error(exp) 21 | raise "Missing process method for sexp: #{exp.inspect}" 22 | end 23 | 24 | def process_proc(exp) 25 | receiver, body = process(exp.shift), exp.shift 26 | return process(body) 27 | end 28 | 29 | def process_dasgn_curr(exp) 30 | @receiver = exp.shift 31 | return @receiver.to_s 32 | end 33 | 34 | def process_array(exp) 35 | arrayed = exp.map { |m| process(m) } 36 | exp.clear 37 | return arrayed.join(', ') 38 | end 39 | 40 | ## 41 | # Helper methods 42 | def to_s 43 | process(@block.to_sexp).squeeze(' ') 44 | end 45 | 46 | def sanitize(value) 47 | case value.to_s 48 | when 'true' then '1' 49 | when 'false' then '0' 50 | else ActiveRecord::Base.connection.quote(value) rescue quote(value) 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/ambition/lib/ambition/query.rb: -------------------------------------------------------------------------------- 1 | module Ambition 2 | class Query 3 | @@select = 'SELECT * FROM %s %s' 4 | 5 | def initialize(owner) 6 | @table_name = owner.table_name 7 | @owner = owner 8 | @clauses = [] 9 | end 10 | 11 | def add(clause) 12 | @clauses << clause 13 | self 14 | end 15 | 16 | def query_context 17 | self 18 | end 19 | 20 | def method_missing(method, *args, &block) 21 | with_context do 22 | @owner.send(method, *args, &block) 23 | end 24 | end 25 | 26 | def with_context 27 | @owner.query_context = self 28 | ret = yield 29 | ensure 30 | @owner.query_context = nil 31 | ret 32 | end 33 | 34 | def to_hash 35 | keyed = keyed_clauses 36 | hash = {} 37 | 38 | unless (where = keyed[:conditions]).blank? 39 | hash[:conditions] = Array(where) 40 | hash[:conditions] *= ' AND ' 41 | end 42 | 43 | unless (includes = keyed[:includes]).blank? 44 | hash[:includes] = includes.flatten 45 | end 46 | 47 | if order = keyed[:order] 48 | hash[:order] = order.join(', ') 49 | end 50 | 51 | if limit = keyed[:limit] 52 | hash[:limit] = limit.join(', ') 53 | end 54 | 55 | hash 56 | end 57 | 58 | def to_s 59 | hash = keyed_clauses 60 | 61 | sql = [] 62 | sql << "JOIN #{hash[:includes].join(', ')}" unless hash[:includes].blank? 63 | sql << "WHERE #{hash[:conditions].join(' AND ')}" unless hash[:conditions].blank? 64 | sql << "ORDER BY #{hash[:order].join(', ')}" unless hash[:order].blank? 65 | sql << "LIMIT #{hash[:limit].join(', ')}" unless hash[:limit].blank? 66 | 67 | @@select % [ @table_name, sql.join(' ') ] 68 | end 69 | alias_method :to_sql, :to_s 70 | 71 | def keyed_clauses 72 | @clauses.inject({}) do |hash, clause| 73 | hash[clause.key] ||= [] 74 | hash[clause.key] << clause.to_s 75 | 76 | if clause.respond_to?(:includes) && !clause.includes.blank? 77 | hash[:includes] ||= [] 78 | hash[:includes] << clause.includes 79 | end 80 | 81 | hash 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/ambition/lib/ambition/where.rb: -------------------------------------------------------------------------------- 1 | module Ambition 2 | module Where 3 | def select(*args, &block) 4 | ## 5 | # XXX: AR::Base hack / workaround 6 | if args.empty? 7 | query_context.add WhereProcessor.new(self, block) 8 | else 9 | super 10 | end 11 | end 12 | 13 | def detect(&block) 14 | select(&block).first 15 | end 16 | end 17 | 18 | class WhereProcessor < Processor 19 | attr_reader :includes 20 | 21 | def initialize(owner, block) 22 | super() 23 | @receiver = nil 24 | @owner = owner 25 | @table_name = owner.table_name 26 | @block = block 27 | @key = :conditions 28 | @includes = [] 29 | end 30 | 31 | ## 32 | # Sexp Processing Methods 33 | def process_and(exp) 34 | joined_expressions 'AND', exp 35 | end 36 | 37 | def process_or(exp) 38 | joined_expressions 'OR', exp 39 | end 40 | 41 | def process_not(exp) 42 | _, receiver, method, other = *exp.first 43 | exp.clear 44 | return translation(receiver, negate(method), other) 45 | end 46 | 47 | def process_call(exp) 48 | receiver, method, other = *exp 49 | exp.clear 50 | 51 | return translation(receiver, method, other) 52 | end 53 | 54 | def process_lit(exp) 55 | exp.shift.to_s 56 | end 57 | 58 | def process_str(exp) 59 | sanitize exp.shift 60 | end 61 | 62 | def process_nil(exp) 63 | 'NULL' 64 | end 65 | 66 | def process_false(exp) 67 | sanitize 'false' 68 | end 69 | 70 | def process_true(exp) 71 | sanitize 'true' 72 | end 73 | 74 | def process_match3(exp) 75 | regexp, target = exp.shift.last.inspect.gsub('/',''), process(exp.shift) 76 | "#{target} REGEXP '#{regexp}'" 77 | end 78 | 79 | def process_dvar(exp) 80 | target = exp.shift 81 | if target == @receiver 82 | return @table_name 83 | else 84 | return value(target.to_s[0..-1]) 85 | end 86 | end 87 | 88 | def process_ivar(exp) 89 | value(exp.shift.to_s[0..-1]) 90 | end 91 | 92 | def process_lvar(exp) 93 | value(exp.shift.to_s) 94 | end 95 | 96 | def process_vcall(exp) 97 | value(exp.shift.to_s) 98 | end 99 | 100 | def process_gvar(exp) 101 | value(exp.shift.to_s) 102 | end 103 | 104 | def process_attrasgn(exp) 105 | exp.clear 106 | raise "Assignment not supported. Maybe you meant ==?" 107 | end 108 | 109 | ## 110 | # Processor helper methods 111 | def joined_expressions(with, exp) 112 | clauses = [] 113 | while clause = exp.shift 114 | clauses << clause 115 | end 116 | return "(" + clauses.map { |c| process(c) }.join(" #{with} ") + ")" 117 | end 118 | 119 | def value(variable) 120 | sanitize eval(variable, @block) 121 | end 122 | 123 | def negate(method) 124 | case method 125 | when :== 126 | '<>' 127 | when :=~ 128 | '!~' 129 | else 130 | raise "Not implemented: #{method}" 131 | end 132 | end 133 | 134 | def translation(receiver, method, other) 135 | case method.to_s 136 | when '==' 137 | "#{process(receiver)} = #{process(other)}" 138 | when '<>', '>', '<' 139 | "#{process(receiver)} #{method} #{process(other)}" 140 | when 'include?' 141 | "#{process(other)} IN (#{process(receiver)})" 142 | when '=~' 143 | "#{process(receiver)} LIKE #{process(other)}" 144 | when '!~' 145 | "#{process(receiver)} NOT LIKE #{process(other)}" 146 | else 147 | build_condition(receiver, method, other) 148 | end 149 | end 150 | 151 | def build_condition(receiver, method, other) 152 | if receiver.first == :call && receiver[1].last == @receiver 153 | if reflection = @owner.reflections[receiver.last] 154 | @includes << reflection.name unless @includes.include? reflection.name 155 | "#{reflection.table_name}.#{method}" 156 | else 157 | raise "No reflection `#{receiver.last}' found on #{@owner}" 158 | end 159 | else 160 | "#{process(receiver)}.`#{method}` #{process(other)}" 161 | end 162 | end 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /lib/ambition/lib/proc_to_ruby.rb: -------------------------------------------------------------------------------- 1 | ## 2 | # Taken from ruby2ruby, Copyright (c) 2006 Ryan Davis under the MIT License 3 | require 'parse_tree' 4 | require 'unique' 5 | require 'sexp_processor' 6 | 7 | class Method 8 | def with_class_and_method_name 9 | if self.inspect =~ // then 10 | klass = eval $1 11 | method = $2.intern 12 | raise "Couldn't determine class from #{self.inspect}" if klass.nil? 13 | return yield(klass, method) 14 | else 15 | raise "Can't parse signature: #{self.inspect}" 16 | end 17 | end 18 | 19 | def to_sexp 20 | with_class_and_method_name do |klass, method| 21 | ParseTree.new(false).parse_tree_for_method(klass, method) 22 | end 23 | end 24 | end 25 | 26 | class Proc 27 | def to_method 28 | Unique.send(:define_method, :proc_to_method, self) 29 | Unique.new.method(:proc_to_method) 30 | end 31 | 32 | def to_sexp 33 | body = self.to_method.to_sexp[2][1..-1] 34 | [:proc, *body] 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/ambition/test/chaining_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "Chaining" do 4 | specify "should join selects with AND" do 5 | sql = User.select { |m| m.name == 'jon' } 6 | sql = sql.select { |m| m.age == 22 } 7 | sql.to_sql.should == "SELECT * FROM users WHERE users.`name` = 'jon' AND users.`age` = 22" 8 | end 9 | 10 | specify "should join sort_bys with a comma" do 11 | sql = User.select { |m| m.name == 'jon' } 12 | sql = sql.sort_by { |m| m.name } 13 | sql = sql.sort_by { |m| m.age } 14 | sql.to_sql.should == "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY users.name, users.age" 15 | end 16 | 17 | specify "should join selects and sorts intelligently" do 18 | sql = User.select { |m| m.name == 'jon' } 19 | sql = sql.select { |m| m.age == 22 } 20 | sql = sql.sort_by { |m| -m.name } 21 | sql = sql.sort_by { |m| m.age } 22 | sql.to_sql.should == "SELECT * FROM users WHERE users.`name` = 'jon' AND users.`age` = 22 ORDER BY users.name DESC, users.age" 23 | end 24 | 25 | specify "should join lots of selects and sorts intelligently" do 26 | sql = User.select { |m| m.name == 'jon' } 27 | sql = sql.select { |m| m.age == 22 } 28 | sql = sql.sort_by { |m| m.name } 29 | sql = sql.select { |m| m.power == true } 30 | sql = sql.sort_by { |m| m.email } 31 | sql = sql.select { |m| m.admin == true && m.email == 'chris@ozmm.org' } 32 | sql.to_sql.should == "SELECT * FROM users WHERE users.`name` = 'jon' AND users.`age` = 22 AND users.`power` = 1 AND (users.`admin` = 1 AND users.`email` = 'chris@ozmm.org') ORDER BY users.name, users.email" 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/ambition/test/count_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "Count" do 4 | setup do 5 | hash = { :conditions => "users.`name` = 'jon'" } 6 | User.expects(:count).with(hash) 7 | @sql = User.select { |m| m.name == 'jon' } 8 | end 9 | 10 | specify "size" do 11 | @sql.size 12 | end 13 | 14 | specify "length" do 15 | @sql.length 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ambition/test/enumerable_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "Each" do 4 | specify "simple ==" do 5 | hash = { :conditions => "users.`age` = 21" } 6 | User.expects(:find).with(:all, hash).returns([]) 7 | User.select { |m| m.age == 21 }.each do |user| 8 | puts user.name 9 | end 10 | end 11 | 12 | specify "limit and conditions" do 13 | hash = { :limit => '5', :conditions => "users.`age` = 21" } 14 | User.expects(:find).with(:all, hash).returns([]) 15 | User.select { |m| m.age == 21 }.first(5).each do |user| 16 | puts user.name 17 | end 18 | end 19 | 20 | specify "limit and conditions and order" do 21 | hash = { :limit => '5', :conditions => "users.`age` = 21", :order => 'users.name' } 22 | User.expects(:find).with(:all, hash).returns([]) 23 | User.select { |m| m.age == 21 }.sort_by { |m| m.name }.first(5).each do |user| 24 | puts user.name 25 | end 26 | end 27 | 28 | specify "limit and order" do 29 | hash = { :limit => '5', :order => 'users.name' } 30 | User.expects(:find).with(:all, hash).returns([]) 31 | User.sort_by { |m| m.name }.first(5).each do |user| 32 | puts user.name 33 | end 34 | end 35 | end 36 | 37 | context "Enumerable Methods" do 38 | specify "map" do 39 | hash = { :conditions => "users.`age` = 21" } 40 | User.expects(:find).with(:all, hash).returns([]) 41 | User.select { |m| m.age == 21 }.map { |u| u.name } 42 | end 43 | 44 | specify "each_with_index" do 45 | hash = { :conditions => "users.`age` = 21" } 46 | User.expects(:find).with(:all, hash).returns([]) 47 | User.select { |m| m.age == 21 }.each_with_index do |user, i| 48 | puts "#{i}: #{user.name}" 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/ambition/test/helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'test/spec' 3 | require 'mocha' 4 | require 'redgreen' 5 | require 'active_support' 6 | require 'active_record' 7 | 8 | $:.unshift File.dirname(__FILE__) + '/../lib' 9 | require 'ambition' 10 | 11 | class User 12 | extend Ambition 13 | 14 | def self.reflections 15 | return @reflections if @reflections 16 | @reflections = {} 17 | @reflections[:ideas] = Reflection.new(:has_many, 'user_id', :ideas, 'ideas') 18 | @reflections[:invites] = Reflection.new(:has_many, 'referrer_id', :invites, 'invites') 19 | @reflections[:profile] = Reflection.new(:has_one, 'user_id', :profile, 'profiles') 20 | @reflections[:account] = Reflection.new(:belongs_to, 'account_id', :account, 'accounts') 21 | @reflections 22 | end 23 | 24 | def self.table_name 25 | 'users' 26 | end 27 | end 28 | 29 | class Reflection < Struct.new(:macro, :primary_key_name, :name, :table_name) 30 | end 31 | -------------------------------------------------------------------------------- /lib/ambition/test/join_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "Joins" do 4 | specify "simple == on an association" do 5 | sql = User.select { |m| m.account.email == 'chris@ozmm.org' } 6 | sql.to_hash.should == { 7 | :conditions => "accounts.email = 'chris@ozmm.org'", 8 | :includes => [:account] 9 | } 10 | end 11 | 12 | specify "simple mixed == on an association" do 13 | sql = User.select { |m| m.name == 'chris' && m.account.email == 'chris@ozmm.org' } 14 | sql.to_hash.should == { 15 | :conditions => "(users.`name` = 'chris' AND accounts.email = 'chris@ozmm.org')", 16 | :includes => [:account] 17 | } 18 | end 19 | 20 | specify "multiple associations" do 21 | sql = User.select { |m| m.ideas.title == 'New Freezer' || m.invites.email == 'pj@hyett.com' } 22 | sql.to_hash.should == { 23 | :conditions => "(ideas.title = 'New Freezer' OR invites.email = 'pj@hyett.com')", 24 | :includes => [:ideas, :invites] 25 | } 26 | end 27 | 28 | specify "non-existant associations" do 29 | sql = User.select { |m| m.liquor.brand == 'Jack' } 30 | should.raise { sql.to_hash } 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/ambition/test/limit_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "Limit" do 4 | setup do 5 | @sql = User.select { |m| m.name == 'jon' } 6 | end 7 | 8 | specify "first" do 9 | conditions = { :conditions => "users.`name` = 'jon'", :limit => '1' } 10 | User.expects(:find).with(:first, conditions) 11 | @sql.first 12 | end 13 | 14 | specify "first with argument" do 15 | conditions = { :conditions => "users.`name` = 'jon'", :limit => '5' } 16 | User.expects(:find).with(:all, conditions) 17 | @sql.first(5) 18 | end 19 | 20 | specify "[] with one element" do 21 | conditions = { :conditions => "users.`name` = 'jon'", :limit => '10, 1' } 22 | User.expects(:find).with(:all, conditions) 23 | @sql[10] 24 | end 25 | 26 | specify "[] with two elements" do 27 | conditions = { :conditions => "users.`name` = 'jon'", :limit => '10, 20' } 28 | User.expects(:find).with(:all, conditions) 29 | @sql[10, 20] 30 | end 31 | 32 | specify "[] with range" do 33 | conditions = { :conditions => "users.`name` = 'jon'", :limit => '10, 10' } 34 | User.expects(:find).with(:all, conditions) 35 | @sql[10..20] 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/ambition/test/order_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "Order" do 4 | setup do 5 | @sql = User.select { |m| m.name == 'jon' } 6 | end 7 | 8 | specify "simple order" do 9 | string = @sql.sort_by { |m| m.name }.to_sql 10 | string.should == "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY users.name" 11 | end 12 | 13 | specify "simple combined order" do 14 | string = @sql.sort_by { |m| [ m.name, m.age ] }.to_sql 15 | string.should == "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY users.name, users.age" 16 | end 17 | 18 | specify "simple combined order with single reverse" do 19 | string = @sql.sort_by { |m| [ m.name, -m.age ] }.to_sql 20 | string.should == "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY users.name, users.age DESC" 21 | end 22 | 23 | specify "simple combined order with two reverses" do 24 | string = @sql.sort_by { |m| [ -m.name, -m.age ] }.to_sql 25 | string.should == "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY users.name DESC, users.age DESC" 26 | end 27 | 28 | specify "reverse order with -" do 29 | string = @sql.sort_by { |m| -m.age }.to_sql 30 | string.should == "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY users.age DESC" 31 | end 32 | 33 | xspecify "reverse order with #reverse" do 34 | # TODO: not implemented 35 | string = @sql.sort_by { |m| m.age }.reverse.to_sql 36 | string.should == "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY users.age DESC" 37 | end 38 | 39 | specify "random order" do 40 | string = @sql.sort_by { rand }.to_sql 41 | string.should == "SELECT * FROM users WHERE users.`name` = 'jon' ORDER BY RAND()" 42 | end 43 | 44 | specify "Symbol#to_proc" do 45 | string = User.sort_by(&:name).to_sql 46 | string.should == "SELECT * FROM users ORDER BY users.name" 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/ambition/test/types_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | ## 4 | # Once dynamically, once hardcoded 5 | context "Different types" do 6 | types_hash = { 7 | 'string' => "'string'", 8 | :symbol => "'--- :symbol\n'", 9 | 1 => '1', 10 | 1.2 => '1.2', 11 | nil => 'NULL', 12 | true => '1', 13 | false => '0', 14 | Time.now => "'#{Time.now.to_s(:db)}'", 15 | DateTime.now => "'#{DateTime.now.to_s(:db)}'", 16 | Date.today => "'#{Date.today.to_s(:db)}'" 17 | } 18 | 19 | types_hash.each do |type, translation| 20 | specify "simple using #{type}" do 21 | sql = User.select { |m| m.name == type }.to_sql 22 | sql.should == "SELECT * FROM users WHERE users.`name` = #{translation}" 23 | end 24 | end 25 | 26 | specify "float" do 27 | sql = User.select { |m| m.name == 1.2 }.to_sql 28 | sql.should == "SELECT * FROM users WHERE users.`name` = 1.2" 29 | end 30 | 31 | specify "integer" do 32 | sql = User.select { |m| m.name == 1 }.to_sql 33 | sql.should == "SELECT * FROM users WHERE users.`name` = 1" 34 | end 35 | 36 | specify "true" do 37 | sql = User.select { |m| m.name == true }.to_sql 38 | sql.should == "SELECT * FROM users WHERE users.`name` = 1" 39 | end 40 | 41 | specify "false" do 42 | sql = User.select { |m| m.name == false }.to_sql 43 | sql.should == "SELECT * FROM users WHERE users.`name` = 0" 44 | end 45 | 46 | specify "nil" do 47 | sql = User.select { |m| m.name == nil }.to_sql 48 | sql.should == "SELECT * FROM users WHERE users.`name` = NULL" 49 | end 50 | 51 | xspecify "Time" do 52 | # TODO: nothing but variables inside blocks for now 53 | sql = User.select { |m| m.name == Time.now }.to_sql 54 | sql.should == "SELECT * FROM users WHERE users.`name` = NULL" 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/ambition/test/where_test.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/helper' 2 | 3 | context "Where (using select)" do 4 | specify "simple ==" do 5 | sql = User.select { |m| m.name == 'jon' }.to_sql 6 | sql.should == "SELECT * FROM users WHERE users.`name` = 'jon'" 7 | end 8 | 9 | specify "simple !=" do 10 | sql = User.select { |m| m.name != 'jon' }.to_sql 11 | sql.should == "SELECT * FROM users WHERE users.`name` <> 'jon'" 12 | end 13 | 14 | specify "simple == && ==" do 15 | sql = User.select { |m| m.name == 'jon' && m.age == 21 }.to_sql 16 | sql.should == "SELECT * FROM users WHERE (users.`name` = 'jon' AND users.`age` = 21)" 17 | end 18 | 19 | specify "simple == || ==" do 20 | sql = User.select { |m| m.name == 'jon' || m.age == 21 }.to_sql 21 | sql.should == "SELECT * FROM users WHERE (users.`name` = 'jon' OR users.`age` = 21)" 22 | end 23 | 24 | specify "mixed && and ||" do 25 | sql = User.select { |m| m.name == 'jon' || m.age == 21 && m.password == 'pass' }.to_sql 26 | sql.should == "SELECT * FROM users WHERE (users.`name` = 'jon' OR (users.`age` = 21 AND users.`password` = 'pass'))" 27 | end 28 | 29 | specify "grouped && and ||" do 30 | sql = User.select { |m| (m.name == 'jon' || m.name == 'rick') && m.age == 21 }.to_sql 31 | sql.should == "SELECT * FROM users WHERE ((users.`name` = 'jon' OR users.`name` = 'rick') AND users.`age` = 21)" 32 | end 33 | 34 | specify "simple >/<" do 35 | sql = User.select { |m| m.age > 21 }.to_sql 36 | sql.should == "SELECT * FROM users WHERE users.`age` > 21" 37 | 38 | sql = User.select { |m| m.age < 21 }.to_sql 39 | sql.should == "SELECT * FROM users WHERE users.`age` < 21" 40 | end 41 | 42 | specify "array.include? item" do 43 | sql = User.select { |m| [1, 2, 3, 4].include? m.id }.to_sql 44 | sql.should == "SELECT * FROM users WHERE users.`id` IN (1, 2, 3, 4)" 45 | end 46 | 47 | specify "simple == with variables" do 48 | me = 'chris' 49 | sql = User.select { |m| m.name == me }.to_sql 50 | sql.should == "SELECT * FROM users WHERE users.`name` = '#{me}'" 51 | end 52 | 53 | specify "simple == with method arguments" do 54 | def test_it(name) 55 | sql = User.select { |m| m.name == name }.to_sql 56 | sql.should == "SELECT * FROM users WHERE users.`name` = '#{name}'" 57 | end 58 | 59 | test_it('chris') 60 | end 61 | 62 | specify "simple == with instance variables" do 63 | @me = 'chris' 64 | sql = User.select { |m| m.name == @me }.to_sql 65 | sql.should == "SELECT * FROM users WHERE users.`name` = '#{@me}'" 66 | end 67 | 68 | xspecify "simple == with instance variable method call" do 69 | require 'ostruct' 70 | @person = OpenStruct.new(:name => 'chris') 71 | 72 | sql = User.select { |m| m.name == @person.name }.to_sql 73 | sql.should == "SELECT * FROM users WHERE users.`name` = '#{@person.name}'" 74 | end 75 | 76 | specify "simple == with global variables" do 77 | $my_name = 'boston' 78 | sql = User.select { |m| m.name == $my_name }.to_sql 79 | sql.should == "SELECT * FROM users WHERE users.`name` = '#{$my_name}'" 80 | end 81 | 82 | specify "simple == with method call" do 83 | def band 84 | 'megadeth' 85 | end 86 | 87 | sql = User.select { |m| m.name == band }.to_sql 88 | sql.should == "SELECT * FROM users WHERE users.`name` = '#{band}'" 89 | end 90 | 91 | specify "simple =~ with string" do 92 | sql = User.select { |m| m.name =~ 'chris' }.to_sql 93 | sql.should == "SELECT * FROM users WHERE users.`name` LIKE 'chris'" 94 | 95 | sql = User.select { |m| m.name =~ 'chri%' }.to_sql 96 | sql.should == "SELECT * FROM users WHERE users.`name` LIKE 'chri%'" 97 | end 98 | 99 | specify "simple !~ with string" do 100 | sql = User.select { |m| m.name !~ 'chris' }.to_sql 101 | sql.should == "SELECT * FROM users WHERE users.`name` NOT LIKE 'chris'" 102 | 103 | sql = User.select { |m| !(m.name =~ 'chris') }.to_sql 104 | sql.should == "SELECT * FROM users WHERE users.`name` NOT LIKE 'chris'" 105 | end 106 | 107 | specify "simple =~ with regexp" do 108 | sql = User.select { |m| m.name =~ /chris/ }.to_sql 109 | sql.should == "SELECT * FROM users WHERE users.`name` REGEXP 'chris'" 110 | end 111 | 112 | xspecify "simple == with inline ruby" do 113 | # TODO: implement this 114 | sql = User.select { |m| m.created_at == 2.days.ago.to_s(:db) }.to_sql 115 | sql.should == "SELECT * FROM users WHERE users.`created_at` = #{2.days.ago.to_s(:db)}" 116 | end 117 | end 118 | 119 | context "Where (using detect)" do 120 | specify "simple ==" do 121 | conditions = { :conditions => "users.`name` = 'chris'", :limit => '1' } 122 | User.expects(:find).with(:first, conditions) 123 | User.detect { |m| m.name == 'chris' } 124 | end 125 | 126 | specify "nothing found" do 127 | conditions = { :conditions => "users.`name` = 'chris'", :limit => '1' } 128 | User.expects(:find).with(:first, conditions).returns(nil) 129 | User.detect { |m| m.name == 'chris' }.should.be.nil 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/cheat.rb: -------------------------------------------------------------------------------- 1 | %w( tempfile fileutils net/http yaml open-uri cheat/wrap ).each { |f| require f } 2 | require 'pager' 3 | RUBY_PLATFORM = PLATFORM unless defined? RUBY_PLATFORM # Ruby 1.8 compatibility 4 | 5 | def mswin? 6 | (RUBY_PLATFORM =~ /(:?mswin|mingw)/) || (RUBY_PLATFORM == 'java' && (ENV['OS'] || ENV['os']) =~ /windows/i) 7 | end 8 | 9 | module Cheat 10 | include Pager 11 | extend self 12 | 13 | HOST = ARGV.include?('debug') ? 'localhost' : 'cheat.errtheblog.com' 14 | PORT = ARGV.include?('debug') ? 3001 : 80 15 | SUFFIX = '' 16 | 17 | def sheets(args) 18 | args = args.dup 19 | 20 | return unless parse_args(args) 21 | 22 | FileUtils.mkdir(cache_dir) unless File.exists?(cache_dir) if cache_dir 23 | 24 | uri = "http://#{cheat_uri}/y/" 25 | 26 | if @offline 27 | return process(File.read(cache_file)) if File.exists?(cache_file) rescue clear_cache if cache_file 28 | else 29 | if %w[sheets all recent].include? @sheet 30 | uri = uri.sub('/y/', @sheet == 'recent' ? '/yr/' : '/ya/') 31 | return open(uri, headers) { |body| process(body.read) } 32 | end 33 | return process(File.read(cache_file)) if File.exists?(cache_file) rescue clear_cache if cache_file 34 | fetch_sheet(uri + @sheet) if @sheet 35 | end 36 | end 37 | 38 | def fetch_sheet(uri, try_to_cache = true) 39 | open(uri, headers) do |body| 40 | sheet = body.read 41 | FileUtils.mkdir_p(cache_dir) unless File.exists?(cache_dir) 42 | File.open(cache_file, 'w') { |f| f.write(sheet) } if try_to_cache && has_content(sheet) && cache_file && !@edit 43 | @edit ? edit(sheet) : show(sheet) 44 | end 45 | exit 46 | rescue OpenURI::HTTPError => e 47 | puts "Whoa, some kind of Internets error!", "=> #{e} from #{uri}" 48 | end 49 | 50 | def parse_args(args) 51 | puts "Looking for help? Try http://cheat.errtheblog.com or `$ cheat cheat'" and return if args.empty? 52 | 53 | if args.delete('--clear-cache') || args.delete('--new') 54 | clear_cache 55 | return if args.empty? 56 | end 57 | 58 | if i = args.index('--diff') 59 | diff_sheets(args.first, args[i+1]) 60 | end 61 | 62 | show_versions(args.first) if args.delete('--versions') 63 | 64 | list if args.delete('--list') 65 | 66 | add(args.shift) and return if args.delete('--add') 67 | incoming_file = true if @edit = args.delete('--edit') 68 | 69 | @execute = true if args.delete("--execute") || args.delete("-x") 70 | # use offline (use cached versions only) if no active connection to internet 71 | @offline = true if args.delete("--local") || args.delete("-l") 72 | @sheet = args.shift 73 | 74 | clear_cache_file if incoming_file 75 | true 76 | end 77 | 78 | # $ cheat greader --versions 79 | def show_versions(sheet) 80 | fetch_sheet("http://#{cheat_uri}/h/#{sheet}/", false) 81 | end 82 | 83 | # $ cheat greader --diff 1[:3] 84 | def diff_sheets(sheet, version) 85 | return unless version =~ /^(\d+)(:(\d+))?$/ 86 | old_version, new_version = $1, $3 87 | 88 | uri = "http://#{cheat_uri}/d/#{sheet}/#{old_version}" 89 | uri += "/#{new_version}" if new_version 90 | 91 | fetch_sheet(uri, false) 92 | end 93 | 94 | def has_content(sheet) 95 | if sheet.is_a?(String) 96 | return (sheet.length > 15) && !sheet[0,14].include?("Error!") 97 | end 98 | return true 99 | end 100 | 101 | def cache_file 102 | "#{cache_dir}/#{@sheet}.yml" if cache_dir 103 | end 104 | 105 | def headers 106 | { 'User-Agent' => 'cheat!', 'Accept' => 'text/yaml' } 107 | end 108 | 109 | def cheat_uri 110 | "#{HOST}:#{PORT}#{SUFFIX}" 111 | end 112 | 113 | def execute(sheet_yaml) 114 | sheet_body = YAML.load(sheet_yaml).to_a.flatten.last 115 | puts "\n " + sheet_body.gsub("\r",'').gsub("\n", "\n ").wrap 116 | puts "\nWould you like to execute the above sheet? (Y/N)" 117 | answer = STDIN.gets 118 | case answer.chomp 119 | when "Y" then system YAML.load(sheet_yaml).to_a.flatten.last 120 | when "N" then puts "Not executing sheet." 121 | else 122 | puts "Must be Y or N!" 123 | end 124 | rescue Errno::EPIPE 125 | # do nothing 126 | rescue 127 | puts "That didn't work. Maybe try `$ cheat cheat' for help?" # Fix Emacs ruby-mode highlighting bug: `" 128 | end 129 | 130 | def process(sheet_yaml) 131 | if @execute 132 | execute(sheet_yaml) 133 | else 134 | show(sheet_yaml) 135 | end 136 | end 137 | 138 | def list 139 | if cache_dir 140 | d = Dir.glob "#{cache_dir}/#{@sheet}*.yml" 141 | d.each {|f| puts File.basename(f, ".yml")} 142 | end 143 | end 144 | 145 | def show(sheet_yaml) 146 | sheet = YAML.load(sheet_yaml).to_a.first 147 | sheet[-1] = sheet.last.join("\n") if sheet[-1].is_a?(Array) 148 | page 149 | puts sheet.first + ':' 150 | puts ' ' + sheet.last.gsub("\r",'').gsub("\n", "\n ").wrap 151 | rescue Errno::EPIPE 152 | # do nothing 153 | rescue 154 | puts "That didn't work. Maybe try `$ cheat cheat' for help?" # Fix Emacs ruby-mode highlighting bug: `" 155 | end 156 | 157 | def edit(sheet_yaml) 158 | sheet = YAML.load(sheet_yaml).to_a.first 159 | sheet[-1] = sheet.last.gsub("\r", '') 160 | body, title = write_to_tempfile(*sheet), sheet.first 161 | return if body.strip == sheet.last.strip 162 | res = post_sheet(title, body) 163 | check_errors(res, title, body) 164 | end 165 | 166 | def add(title) 167 | body = write_to_tempfile(title) 168 | res = post_sheet(title, body, true) 169 | check_errors(res, title, body) 170 | end 171 | 172 | def post_sheet(title, body, new = false) 173 | uri = "http://#{cheat_uri}/w/" 174 | uri += title unless new 175 | Net::HTTP.post_form(URI.parse(uri), "sheet_title" => title, "sheet_body" => body.strip, "from_gem" => true) 176 | end 177 | 178 | def write_to_tempfile(title, body = nil) 179 | # god dammit i hate tempfile, this is so messy but i think it's 180 | # the only way. 181 | tempfile = Tempfile.new(title + '.cheat') 182 | tempfile.write(body) if body 183 | tempfile.close 184 | system "#{editor} #{tempfile.path}" 185 | tempfile.open 186 | body = tempfile.read 187 | tempfile.close 188 | body 189 | end 190 | 191 | def check_errors(result, title, text) 192 | if result.body =~ /

(.+?)<\/p>/m 193 | puts $1.gsub(/\n/, '').gsub(/<.+?>/, '').squeeze(' ').wrap(80) 194 | puts 195 | puts "Here's what you wrote, so it isn't lost in the void:" 196 | puts text 197 | else 198 | puts "Success! Try it!", "$ cheat #{title}" 199 | end 200 | end 201 | 202 | def editor 203 | ENV['VISUAL'] || ENV['EDITOR'] || "vim" 204 | end 205 | 206 | def cache_dir 207 | mswin? ? win32_cache_dir : File.join(File.expand_path("~"), ".cheat") 208 | end 209 | 210 | def win32_cache_dir 211 | unless File.exists?(home = ENV['HOMEDRIVE'] + ENV['HOMEPATH']) 212 | puts "No HOMEDRIVE or HOMEPATH environment variable. Set one to save a" + 213 | "local cache of cheat sheets." 214 | return false 215 | else 216 | return File.join(home, 'Cheat') 217 | end 218 | end 219 | 220 | def clear_cache 221 | FileUtils.rm_rf(cache_dir) if cache_dir 222 | end 223 | 224 | def clear_cache_file 225 | FileUtils.rm(cache_file) if File.exists?(cache_file) 226 | end 227 | 228 | end 229 | 230 | Cheat.sheets(ARGV) if __FILE__ == $0 231 | -------------------------------------------------------------------------------- /lib/cheat/diffr.rb: -------------------------------------------------------------------------------- 1 | require 'diff/lcs' 2 | require 'diff/lcs/hunk' 3 | 4 | module Cheat 5 | class Diffr 6 | def self.diff(sheet_old, sheet_new) 7 | format, lines, output = :unified, 10000, '' 8 | file_length_difference = 0 9 | 10 | data_old = sheet_old.body.wrap.split(/\n/).map! { |e| e.chomp } 11 | data_new = sheet_new.body.wrap.split(/\n/).map! { |e| e.chomp } 12 | 13 | diffs = Diff::LCS.diff(data_old, data_new) 14 | return if diffs.empty? 15 | 16 | header = '' 17 | ft = sheet_old.updated_at 18 | header << "#{'-' * 3} #{sheet_new.title} version #{sheet_old.version}\t#{ft}\n" 19 | ft = sheet_new.updated_at 20 | header << "#{'+' * 3} #{sheet_new.title} version #{sheet_new.version}\t#{ft}\n" 21 | 22 | oldhunk = hunk = nil 23 | 24 | diffs.each do |piece| 25 | begin 26 | hunk = Diff::LCS::Hunk.new(data_old, data_new, piece, lines, file_length_difference) 27 | file_length_difference = hunk.file_length_difference 28 | 29 | next unless oldhunk 30 | 31 | if lines > 0 && hunk.overlaps?(oldhunk) 32 | hunk.unshift(oldhunk) 33 | else 34 | output << oldhunk.diff(format) 35 | end 36 | ensure 37 | oldhunk = hunk 38 | output << "\n" 39 | end 40 | end 41 | 42 | output << oldhunk.diff(format) 43 | output << "\n" 44 | 45 | return header + output.lstrip 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/cheat/responder.rb: -------------------------------------------------------------------------------- 1 | # class Something < R 'route' 2 | # include Responder 3 | # 4 | # def get 5 | # ... important code ... 6 | # 7 | # respond_to do |wants| 8 | # wants.html { render :something } 9 | # wants.text { "Just some text." } 10 | # wants.yaml { "Something neat!".to_yaml } 11 | # wants.xml { "Also, XML.".to_xml } 12 | # end 13 | # end 14 | # end 15 | module Cheat::Controllers 16 | module Responder 17 | def respond_to 18 | yield response = Response.new(env.HTTP_ACCEPT) 19 | @headers['Content-Type'] = response.content_type 20 | response.body 21 | end 22 | 23 | class Response 24 | attr_reader :body, :content_type 25 | def initialize(accept) @accept = accept end 26 | 27 | TYPES = { 28 | :yaml => %w[application/yaml text/yaml], 29 | :text => %w[text/plain], 30 | :html => %w[text/html */* application/html], 31 | :xml => %w[application/xml] 32 | } 33 | 34 | def method_missing(method, *args) 35 | if TYPES[method] && @accept =~ Regexp.union(*TYPES[method]) 36 | @content_type = TYPES[method].first 37 | @body = yield if block_given? 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/cheat/rv_harness.rb: -------------------------------------------------------------------------------- 1 | 2 | # Example mongrel harness for camping apps with rv 3 | # 4 | # author: Evan Weaver 5 | # url: http://blog.evanweaver.com/articles/2006/12/19/rv-a-tool-for-luxurious-camping 6 | # license: AFL 3.0 7 | 8 | require 'mongrel' 9 | require 'mongrel/camping' 10 | LOGFILE = 'mongrel.log' 11 | PIDFILE = 'mongrel.pid' 12 | 13 | # or whatever else you want passed in 14 | PORT = ARGV[0].to_i 15 | ADDR = ARGV[1] 16 | 17 | # this is your camping app 18 | require 'site' 19 | app = Cheat 20 | 21 | # custom database configuration 22 | app::Models::Base.establish_connection :adapter => 'mysql', :user => 'root', :database => 'camping', :host => 'localhost' 23 | 24 | app::Models::Base.logger = nil 25 | app::Models::Base.threaded_connections = false 26 | app.create 27 | 28 | config = Mongrel::Configurator.new :host => ADDR, :pid_file => PIDFILE do 29 | listener :port => PORT do 30 | uri '/', :handler => Mongrel::Camping::CampingHandler.new(app) 31 | # use the mongrel static server in production instead of the camping controller 32 | uri '/static/', :handler => Mongrel::DirHandler.new("static/") 33 | uri '/favicon.ico', :handler => Mongrel::Error404Handler.new('') 34 | setup_signals 35 | run 36 | write_pid_file 37 | log "#{app} available at #{ADDR}:#{PORT}" 38 | join 39 | end 40 | end 41 | 42 | -------------------------------------------------------------------------------- /lib/cheat/site.rb: -------------------------------------------------------------------------------- 1 | # 2 | # According to Wikipedia, Cheat can refer to: 3 | # Cheating, to take advantage of a situation by the breaking of accepted rules 4 | # or standards 5 | # Cheating (casino) 6 | # Cheating in poker 7 | # Cheating in online games 8 | # In relationships, to have an affair 9 | # A cheat code, a hidden means of gaining an advantage in a video game 10 | # Cheating, parasitic abuse of symbiotic relationships 11 | # The Cheat, a character in the cartoon series Homestar Runner 12 | # Cheat!, a television show on the G4 network 13 | # The Cheat, a 1915 Cecil B. DeMille movie about a wealthy and domineering 14 | # Asian gentleman taking advantage of an American female 15 | # Cheats, a 2002 comedy, starring Matthew Lawrence and Mary Tyler Moore 16 | # Cheat, a song by The Clash from the UK version of their album The Clash 17 | # Bullshit, sometimes known as "Cheat," a card game 18 | # An alternate term for defection in the prisoner's dilemma in game theory 19 | # Cheat River, a tributary of the Monongahela River in Appalachia; the Cheat 20 | # starts in West Virginia, and flows westward 21 | # Cheat Lake, a nearby resevoir 22 | # Cheat Mountain, one of the highest mountains in the Alleghenies 23 | # 24 | %w[rubygems camping camping/db erb open-uri acts_as_versioned wrap diffr responder ambition].each { |f| require f } 25 | gem 'camping', '>=1.4.152' 26 | 27 | Camping.goes :Cheat 28 | 29 | # for defunkt. campistrano. 30 | if ARGV.include? '--update' 31 | ssh = 'ssh deploy@errtheblog.com' 32 | puts `#{ssh} 'cd /var/www/cheat; svn up'` 33 | system "#{ssh} 'sudo /etc/init.d/rv restart'" 34 | exit 35 | end 36 | 37 | URL = ARGV.include?('debug') ? 'http://localhost:8020' : 'http://cheat.errtheblog.com' 38 | FEED = 'http://feeds.feedburner.com/cheatsheets' # rss feed 39 | 40 | module Cheat::Models 41 | class Sheet < Base 42 | validates_uniqueness_of :title 43 | validates_format_of :title, :with => /^[a-z]+[a-z0-9_]*$/i 44 | validates_presence_of :title, :body 45 | before_save { |r| r.title = r.title.gsub(' ', '_').underscore.downcase } 46 | acts_as_versioned 47 | end 48 | 49 | class SetUpUsTheCheat < V 1.0 50 | def self.up 51 | create_table :cheat_sheets, :force => true do |t| 52 | t.column :id, :integer, :null => false 53 | t.column :title, :string, :null => false 54 | t.column :body, :text 55 | t.column :created_at, :datetime, :null => false 56 | t.column :updated_at, :datetime, :null => false 57 | end 58 | Sheet.create_versioned_table 59 | Sheet.reset_column_information 60 | end 61 | def self.down 62 | drop_table :cheat_sheets 63 | Sheet.drop_versioned_table 64 | end 65 | end 66 | end 67 | 68 | module Cheat::Controllers 69 | class APIShow < R '/y/(\w+)' 70 | def get(title) 71 | @headers['Content-Type'] = 'text/plain' 72 | 73 | sheet = Sheet.detect { |s| s.title == title } 74 | return { 'Error!' => "Cheat sheet `#{title}' not found." }.to_yaml unless sheet 75 | 76 | return { sheet.title => sheet.body }.to_yaml 77 | end 78 | end 79 | 80 | class APIRecent < R '/yr' 81 | def get 82 | @headers['Content-Type'] = 'text/plain' 83 | 84 | sheets = Sheet.sort_by { |s| -s.created_at }.first(15).map(&:title) 85 | return { 'Recent Cheat Sheets' => sheets }.to_yaml 86 | end 87 | end 88 | 89 | class APIAll < R '/ya' 90 | def get 91 | @headers['Content-Type'] = 'text/plain' 92 | 93 | sheets = Sheet.sort_by(&:title).map(&:title) 94 | return { 'All Cheat Sheets' => sheets }.to_yaml 95 | end 96 | end 97 | 98 | class Feed < R '/f' 99 | def get 100 | @headers['Content-Type'] = 'application/xml' 101 | return Cheat::Views.feed 102 | end 103 | end 104 | 105 | class Index < R '/' 106 | def get 107 | render :index 108 | end 109 | end 110 | 111 | class Add < R '/a' 112 | def get 113 | @sheet = Sheet.new 114 | render :add 115 | end 116 | end 117 | 118 | class Edit < R '/e/(\w+)/(\d+)', '/e/(\w+)' 119 | def get(title, version = nil) 120 | @sheet = Sheet.detect { |s| s.title == title } 121 | 122 | @error = "Cheat sheet not found." unless @sheet 123 | unless version.nil? || version == @sheet.version.to_s 124 | @sheet = @sheet.find_version(version) 125 | end 126 | render @error ? :error : :edit 127 | end 128 | end 129 | 130 | class Write < R '/w', '/w/(\w+)' 131 | def post(title = nil) 132 | @sheet = title ? Sheet.find_by_title(title) : Sheet.new 133 | @sheet = title ? Sheet.detect { |s| s.title == title } : Sheet.new 134 | 135 | check_captcha! unless input.from_gem 136 | 137 | if !@error && @sheet.update_attributes(:title => input.sheet_title, :body => input.sheet_body) 138 | redirect "#{URL}/s/#{@sheet.title}" 139 | else 140 | @error = true 141 | render title ? :edit : :add 142 | end 143 | end 144 | 145 | def check_captcha! 146 | @error ||= !(@cookies[:passed] ||= captcha_pass?(input.chunky, input.bacon)) 147 | end 148 | 149 | def captcha_pass?(session, answer) 150 | open("http://captchator.com/captcha/check_answer/#{session}/#{answer}").read.to_i.nonzero? rescue false 151 | end 152 | end 153 | 154 | class Browse < R '/b' 155 | def get 156 | @sheets = Sheet.sort_by(&:title) 157 | render :browse 158 | end 159 | end 160 | 161 | class Show < R '/s/(\w+)', '/s/(\w+)/(\d+)' 162 | def get(title, version = nil) 163 | @sheet = Sheet.detect { |s| s.title == title } 164 | @sheet = @sheet.find_version(version) if version && @sheet 165 | 166 | @sheet ? render(:show) : redirect("#{URL}/b/") 167 | end 168 | end 169 | 170 | # we are going to start consolidating classes with respond_to and what not. 171 | # diff is the first, as the api and the site will use the same code 172 | class Diff < R '/d/(\w+)/(\d+)', '/d/(\w+)/(\d+)/(\d+)' 173 | include Responder 174 | 175 | def get(title, old_version, new_version = nil) 176 | redirect "#{URL}/b/" and return unless old_version.to_i.nonzero? 177 | 178 | @sheet = Sheet.detect { |s| s.title == title } 179 | @old_sheet = @sheet.find_version(old_version) 180 | @new_sheet = (new_version ? @sheet.find_version(new_version) : @sheet) 181 | 182 | @diffed = Diffr.diff(@old_sheet, @new_sheet) rescue nil 183 | 184 | respond_to do |wants| 185 | wants.html { render :diff } 186 | wants.yaml { { @sheet.title => @diffed }.to_yaml } 187 | end 188 | end 189 | end 190 | 191 | class History < R '/h/(\w+)' 192 | include Responder 193 | 194 | def get(title) 195 | if sheets = Sheet.detect { |s| s.title == title } 196 | @sheets = sheets.find_versions(:order => 'version DESC') 197 | end 198 | 199 | respond_to do |wants| 200 | wants.html { render :history } 201 | wants.yaml { { @sheets.first.title => @sheets.map(&:version) }.to_yaml } 202 | end 203 | end 204 | end 205 | end 206 | 207 | module Cheat::Views 208 | def layout 209 | html { 210 | head { 211 | _style 212 | link :href => FEED, :rel => "alternate", :title => "Recently Updated Cheat Sheets", :type => "application/atom+xml" 213 | title @page_title ? "$ cheat #{@page_title}" : "$ command line ruby cheat sheets" 214 | } 215 | body { 216 | div.main { 217 | div.header { 218 | h1 { logo_link 'cheat sheets.' } 219 | code.header @sheet_title ? "$ cheat #{@sheet_title}" : "$ command line ruby cheat sheets" 220 | } 221 | div.content { self << yield } 222 | div.side { _side } 223 | div.clear { '' } 224 | div.footer { _footer } 225 | } 226 | _clicky 227 | } 228 | } 229 | end 230 | 231 | def _clicky 232 | text '' 233 | end 234 | 235 | def error 236 | @page_title = "error" 237 | p "An error:" 238 | code.version @error 239 | p ":(" 240 | end 241 | 242 | def show 243 | @page_title = @sheet.title 244 | @sheet_title = @sheet.title 245 | pre.sheet { text h(@sheet.body.wrap) } 246 | div.version { 247 | text "Version " 248 | strong sheet.version 249 | text ", updated " 250 | text last_updated(@sheet) 251 | text " ago. " 252 | br 253 | text ". o 0 ( " 254 | if @sheet.version == current_sheet.version 255 | a "edit", :href => R(Edit, @sheet.title) 256 | end 257 | if @sheet.version > 1 258 | text " | " 259 | a "previous", :href => R(Show, @sheet.title, @sheet.version - 1) 260 | end 261 | text " | " 262 | a "history", :href => R(History, @sheet.title) 263 | unless @sheet.version == current_sheet.version 264 | text " | " 265 | a "revert to", :href => R(Edit, @sheet.title, @sheet.version) 266 | text " | " 267 | a "current", :href => R(Show, @sheet.title) 268 | end 269 | diff_version = 270 | if @sheet.version == current_sheet.version 271 | @sheet.version == 1 ? nil : @sheet.version - 1 272 | else 273 | @sheet.version 274 | end 275 | if diff_version 276 | text " | " 277 | a "diff", :href => R(Diff, @sheet.title, diff_version) 278 | end 279 | text " )" 280 | } 281 | end 282 | 283 | def diff 284 | @page_title = @sheet.title 285 | @sheet_title = @sheet.title 286 | pre.sheet { color_diff(h(@diffed)) if @diffed } 287 | div.version { 288 | text ". o 0 (" 289 | if @old_sheet.version > 1 290 | a "diff previous", :href => R(Diff, @sheet.title, @old_sheet.version - 1) 291 | text " | " 292 | end 293 | a "history", :href => R(History, @sheet.title) 294 | text " | " 295 | a "current", :href => R(Show, @sheet.title) 296 | text " )" 297 | } 298 | end 299 | 300 | def browse 301 | @page_title = "browse" 302 | p { "Wowzers, we've got #{@sheets.size} cheat sheets hereabouts." } 303 | ul { 304 | @sheets.each do |sheet| 305 | li { sheet_link sheet.title } 306 | end 307 | } 308 | p { 309 | text "Are we missing a cheat sheet? Why don't you do the whole world a favor and " 310 | a "add it", :href => R(Add) 311 | text " yourself!" 312 | } 313 | end 314 | 315 | def history 316 | @page_title = "history" 317 | @sheet_title = @sheets.first.title 318 | h2 @sheets.first.title 319 | ul { 320 | @sheets.each_with_index do |sheet, i| 321 | li { 322 | a "version #{sheet.version}", :href => R(Show, sheet.title, sheet.version) 323 | text " - created " 324 | text last_updated(sheet) 325 | text " ago" 326 | strong " (current)" if i.zero? 327 | text " " 328 | a "(diff to current)", :href => R(Diff, sheet.title, sheet.version) if i.nonzero? 329 | } 330 | end 331 | } 332 | end 333 | 334 | def add 335 | @page_title = "add" 336 | p { 337 | text "Thanks for wanting to add a cheat sheet. If you need an example of 338 | the standard cheat sheet format, check out the " 339 | a "cheat", :href => R(Show, 'cheat') 340 | text " cheat sheet. (There's really no standard format, though)." 341 | } 342 | _form 343 | end 344 | 345 | def edit 346 | @page_title = "edit" 347 | _form 348 | end 349 | 350 | def _form 351 | if @error 352 | p.error { 353 | strong "HEY! " 354 | text "Something is wrong! You can't give your cheat sheet the same name 355 | as another, alphanumeric titles only, and you need to make sure 356 | you filled in all (two) of the fields. Okay?" 357 | } 358 | end 359 | form :method => 'post', :action => R(Write, @sheet.title) do 360 | p { 361 | p { 362 | text 'Cheat Sheet Title: ' 363 | input :value => @sheet.title, :name => 'sheet_title', :size => 30, 364 | :type => 'text' 365 | small " [ no_spaces_alphanumeric_only ]" 366 | } 367 | p { 368 | text 'Cheat Sheet:' 369 | br 370 | textarea @sheet.body, :name => 'sheet_body', :cols => 80, :rows => 30 371 | unless @cookies[:passed] 372 | random = rand(10_000) 373 | br 374 | img :src => "http://captchator.com/captcha/image/#{random}" 375 | input :name => 'chunky', :type => 'hidden', :value => random 376 | input :name => 'bacon', :size => 10, :type => 'text' 377 | end 378 | } 379 | } 380 | p "Your cheat sheet will be editable (fixable) by anyone. Each cheat 381 | sheet is essentially a wiki page. It may also be used by millions of 382 | people for reference purposes from the comfort of their command line. 383 | If this is okay with you, please save." 384 | input :value => "Save the Damn Thing!", :name => "save", :type => 'submit' 385 | end 386 | end 387 | 388 | def index 389 | p { 390 | text "Welcome. You've reached the central repository for " 391 | strong "cheat" 392 | text ", the RubyGem which puts Ruby-centric cheat sheets right into your 393 | terminal. The inaugural blog entry " 394 | a "is here", :href => "http://errtheblog.com/post/23" 395 | text "." 396 | } 397 | p "Get started:" 398 | code "$ gem install cheat" 399 | br 400 | code "$ cheat strftime" 401 | p "A magnificent cheat sheet for Ruby's strftime method will be printed to 402 | your terminal." 403 | p "To get some help on cheat itself:" 404 | code "$ cheat cheat" 405 | p "How meta." 406 | p { 407 | text "Cheat sheets are basically wiki pages accessible from the command 408 | line. You can " 409 | a 'browse', :href => R(Browse) 410 | text ', ' 411 | a 'add', :href => R(Add) 412 | text ', or ' 413 | a 'edit', :href => R(Edit, 'cheat') 414 | text ' cheat sheets. Try to keep them concise. For a style guide, check 415 | out the ' 416 | a 'cheat', :href => R(Edit, 'cheat') 417 | text ' cheat sheet.' 418 | } 419 | p "To access a cheat sheet, simply pass the program the desired sheet's 420 | name:" 421 | code "$ cheat " 422 | p 423 | end 424 | 425 | def self.feed 426 | xml = Builder::XmlMarkup.new(:indent => 2) 427 | 428 | xml.instruct! 429 | xml.feed "xmlns"=>"http://www.w3.org/2005/Atom" do 430 | 431 | xml.title "Recently Updated Cheat Sheets" 432 | xml.id URL + '/' 433 | xml.link "rel" => "self", "href" => FEED 434 | 435 | sheets = Cheat::Models::Sheet.sort_by { |s| -s.updated_at }.first(20) 436 | xml.updated sheets.first.updated_at.xmlschema 437 | 438 | sheets.each do |sheet| 439 | xml.entry do 440 | xml.id URL + '/s/' + sheet.title 441 | xml.title sheet.title 442 | xml.author { xml.name "An Anonymous Cheater" } 443 | xml.updated sheet.updated_at.xmlschema 444 | xml.link "rel" => "alternate", "href" => URL + '/s/' + sheet.title 445 | xml.summary "A cheat sheet about #{sheet.title}. Run it: `$ cheat #{sheet.title}'" 446 | xml.content 'type' => 'html' do 447 | xml.text! sheet.body.gsub("\n", '
').gsub("\r", '') 448 | end 449 | end 450 | end 451 | end 452 | end 453 | 454 | def _side 455 | text '( ' 456 | a 'add new', :href => R(Add) 457 | text ' | ' 458 | a 'see all', :href => R(Browse) 459 | text ' )' 460 | ul { 461 | li { strong "updated sheets" } 462 | li do 463 | a :href => FEED do 464 | img(:border => 0, :alt => "Recently Updated Cheat Sheets Feed", :src => "http://errtheblog.com/images/feed.png") 465 | end 466 | end 467 | recent_sheets.each do |sheet| 468 | li { sheet_link sheet.title } 469 | end 470 | } 471 | end 472 | 473 | def _footer 474 | text "Powered by " 475 | a 'Camping', :href => "http://code.whytheluckystiff.net/camping" 476 | text ", " 477 | a 'Mongrel', :href => "http://mongrel.rubyforge.org/" 478 | text " and, to a lesser extent, " 479 | a 'Err the Blog', :href => "http://errtheblog.com/" 480 | text "." 481 | end 482 | 483 | def _style 484 | bg = "#fff" 485 | h1 = "#4fa3da" 486 | link = h1 487 | hover = "#f65077" 488 | dash = hover 489 | version = "#fcf095" 490 | style :type => "text/css" do 491 | text %[ 492 | body { font-family: verdana, sans-serif; background-color: #{bg}; 493 | line-height: 20px; } 494 | a:link, a:visited { color: #{link}; } 495 | a:hover { text-decoration: none; color: #{hover}; } 496 | div.header { border-bottom: 1px dashed #{dash}; } 497 | code.header { margin-left: 30px; font-weight: bold; 498 | background-color: #{version}; } 499 | h1 { font-size: 5em; margin: 0; padding-left: 30px; color: #{h1}; 500 | clear: both; font-weight: bold; letter-spacing: -5px; } 501 | h1 a { text-decoration: none; } 502 | div.main { float: left; width: 100%; } 503 | div.content { float: left; width: 70%; padding: 15px 0 15px 30px; 504 | line-height: 20px; } 505 | div.side { float: left; padding: 10px; text-align: right; 506 | width: 20%; } 507 | div.footer { text-align: center; border-top: 1px dashed #{dash}; 508 | padding-top: 10px; font-size: small; } 509 | div.sheet { font-size: .8em; line-height: 17px; padding: 5px; 510 | font-family: courier, fixed-width; background-color: #e8e8e8; } 511 | pre.sheet { line-height: 15px; } 512 | li { list-style: none; } 513 | div.version { background-color: #{version}; padding: 5px; 514 | width: 450px; margin-top: 50px; } 515 | p.error { background-color: #{version}; padding: 5px; } 516 | div.clear { clear: both; } 517 | div.clear_10 { clear: both; font-size: 10px; line-height: 10px; } 518 | textarea { font-family: courier; } 519 | code { background-color: #{version} } 520 | span.diff_cut { color: #f65077; } 521 | span.diff_add { color: #009933; } 522 | @media print { 523 | .side, .version, .footer { display: none; } 524 | div.content { width: 100%; } 525 | h1 a:link, h1 a:visited { color: #eee;} 526 | .header code { font-size: 18px; background: none; } 527 | div.header { border-bottom: none; } 528 | } 529 | ].gsub(/(\s{2,})/, '').gsub("\n", '') 530 | end 531 | end 532 | end 533 | 534 | module Cheat::Helpers 535 | def logo_link(title) 536 | ctr = Cheat::Controllers 537 | if @sheet && !@sheet.new_record? && @sheet.version != current_sheet.version 538 | a title, :href => R(ctr::Show, @sheet.title) 539 | else 540 | a title, :href => R(ctr::Index) 541 | end 542 | end 543 | 544 | def current_sheet 545 | title = @sheet.title 546 | @current_sheet ||= Cheat::Models::Sheet.detect { |s| s.title == title } 547 | end 548 | 549 | def recent_sheets 550 | Cheat::Models::Sheet.sort_by { |s| -s.updated_at }.first(15) 551 | end 552 | 553 | def sheet_link(title, version = nil) 554 | a title, :href => R(Cheat::Controllers::Show, title, version) 555 | end 556 | 557 | def last_updated(sheet) 558 | from = sheet.updated_at.to_i 559 | to = Time.now.to_i 560 | from = from.to_time if from.respond_to?(:to_time) 561 | to = to.to_time if to.respond_to?(:to_time) 562 | distance = (((to - from).abs)/60).round 563 | case distance 564 | when 0..1 then return (distance==0) ? 'less than a minute' : '1 minute' 565 | when 2..45 then "#{distance} minutes" 566 | when 46..90 then 'about 1 hour' 567 | when 90..1440 then "about #{(distance.to_f / 60.0).round} hours" 568 | when 1441..2880 then '1 day' 569 | else "#{(distance / 1440).round} days" 570 | end 571 | end 572 | 573 | def self.h(text) 574 | ERB::Util::h(text) 575 | end 576 | 577 | def h(text) 578 | ::Cheat::Helpers.h(text) 579 | end 580 | 581 | def color_diff(diff) 582 | diff.split("\n").map do |line| 583 | action = case line 584 | when /^-/ then 'cut' 585 | when /^\+/ then 'add' 586 | end 587 | action ? span.send("diff_#{action}", line) : line 588 | end * "\n" 589 | end 590 | end 591 | 592 | def Cheat.create 593 | Cheat::Models.create_schema 594 | end 595 | 596 | if __FILE__ == $0 597 | begin 598 | require 'mongrel/camping' 599 | rescue LoadError => e 600 | abort "** Try running `camping #$0' instead." 601 | end 602 | 603 | Cheat::Models::Base.establish_connection :adapter => 'mysql', :user => 'root', :database => 'camping', :host => 'localhost' 604 | Cheat::Models::Base.logger = nil 605 | Cheat::Models::Base.threaded_connections = false 606 | Cheat.create 607 | 608 | server = Mongrel::Camping.start("0.0.0.0", 8020, "/", Cheat) 609 | puts "** Cheat is running at http://0.0.0.0:8020/" 610 | server.run.join 611 | end 612 | -------------------------------------------------------------------------------- /lib/cheat/version.rb: -------------------------------------------------------------------------------- 1 | module Cheat 2 | class Version 3 | class << self 4 | 5 | # @return [Integer] 6 | def major 7 | 1 8 | end 9 | 10 | # @return [Integer] 11 | def minor 12 | 3 13 | end 14 | 15 | # @return [Integer] 16 | def patch 17 | 3 18 | end 19 | 20 | # @return [String, NilClass] 21 | def pre 22 | nil 23 | end 24 | 25 | # @return [String] 26 | def to_s 27 | [major, minor, patch, pre].compact.join('.') 28 | end 29 | 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/cheat/wrap.rb: -------------------------------------------------------------------------------- 1 | # >> Evan Weaver 2 | # => http://blog.evanweaver.com/articles/2006/09/03/smart-plaintext-wrapping 3 | class String 4 | def wrap(width = 80, hanging_indent = 0, magic_lists = false) 5 | lines = self.split(/\n/) 6 | 7 | lines.collect! do |line| 8 | 9 | if magic_lists 10 | line =~ /^([\s\-\d\.\:]*\s)/ 11 | else 12 | line =~ /^([\s]*\s)/ 13 | end 14 | 15 | indent = $1.length + hanging_indent rescue hanging_indent 16 | 17 | buffer = "" 18 | first = true 19 | 20 | while line.length > 0 21 | first ? (i, first = 0, false) : i = indent 22 | pos = width - i 23 | 24 | if line.length > pos and line[0..pos] =~ /^(.+)\s/ 25 | subline = $1 26 | else 27 | subline = line[0..pos] 28 | end 29 | buffer += " " * i + subline + "\n" 30 | line.tail!(subline.length) 31 | end 32 | buffer[0..-2] 33 | end 34 | 35 | lines.join("\n") 36 | end 37 | 38 | def tail!(pos) 39 | self[0..pos] = "" 40 | strip! 41 | end 42 | end 43 | --------------------------------------------------------------------------------