Skip to content

Commit

Permalink
initial commit -- lost history before this commit
Browse files Browse the repository at this point in the history
  • Loading branch information
davejacobs committed Sep 20, 2012
1 parent d50f4e2 commit 8e8e2c1
Show file tree
Hide file tree
Showing 12 changed files with 635 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Gemfile.lock
Empty file added COPYING
Empty file.
11 changes: 11 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
source :rubygems

gem "awesome_print"
gem "activesupport"
gem "xml-simple"

if RUBY_VERSION =~ /1\.9\.\d+/
gem "debugger"
else
gem "ruby-debug"
end
96 changes: 96 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
Letters
-------

**Letters** is a little alphabetical library that makes debugging fun.

### Debugging with the alphabet ###

Most engineers have a limited toolkit for debugging. For some, it's actually just the `print` statement. For others, it's `print` + the debugger. Those tools are good, but they are the lowest level of how we can debug in Ruby. With Letters, I want to start to think about `print` and `debugger` as building blocks and not as structures in themselves.

Letters aims sophisticated debugging as easy as typing out the alphabet.

### Installation ###

If you're using RubyGems, install Letters with:

gem install letters

By default, requiring `"letters"` monkey-patches Object. It goes without saying that if you're using Letters in an app that has environments, you probably only want to use it in development.

### Debugging with letters ###

With Letters installed, you have a suite of methods available wherever you want them in your code -- at the end of any expression, in the middle of any pipeline. Most of these methods will output some form of information, though there are more sophisticated ones that pass around control of the application.

Let's start with the `z` method as an example. It is the building block for all other letter methods. Add it to the end of any object to inspect it and return the same object:

{ foo: "bar" }.z
# => { foo: "bar" }
# prints { foo: "bar" }

That's simple enough, but not really useful. Things get interesting when you're in a pipeline:

words.grep(/interesting/).
map(&:downcase).
slice(0..2).
reject {|w| w.length < 3 }.
group_by(&:length).
values_at(5, 10)
join(", ")

If I want to know the state of your code after lines 3 and 5, all I have to do is add `.z` to each one:

words.grep(/interesting/).
map(&:downcase).
slice(0..2).z.
reject {|w| w.length < 3 }.
group_by(&:length).z.
values_at(5, 10)
join(", ")

Because the `z` method (and every other Letters method) returns the original object, introducing it is only ever for side effects -- they won't change the output of your code.

This is significantly easier than breaking apart the pipeline using variable assignment or a hefty `tap` block.

### The current API ###

Here are my past and future plans for Letters. So far, I have implemented all debug functions below except those marked with an asterisk (\*):

*(Note that if you don't want to patch `Hash` and `Array` with such small method names, you can explicitly require "letters/core_ext" instead. Letters::CoreExt will be available for you to `include` in any object or class you'd like. Requiring "letters" on its own will add the alphabet methods to `Hash` and `Array`.)*

- *A* -
- *B* - Beep (for coarse-grained time-analysis)
- *C* - Print callstack
- *D* - Enter the debugger
- *D1/D2 pairs* - diff two data structures or objects
- *E* - Empty check -- raise error if receiver is empty
- *F* - Write to file (format can be default or specified)
- *G* -
- *H* -
- *I* - Gain control from nearest transmitter (with value)\*
- *J* - Jump into object's context (execute methods inside object's context)
- *K* -
- *L* - Logger (Rails or otherwise) -- only works if `logger` is instantiated
- *M* - Mark with message to be printed when object is garbage-collected\*
- *N* - Nil check -- raise error if receiver is nil
- *O* - List all instantiated objects\*
- *P* - Print to STDOUT (format can be default or specified)
- *Q* -
- *R* - RI documentation for class
- *S* - Bump [safety level]()
- *T* - [Taint object]()
- *U* - Untaint object
- *V* -
- *W* -
- *X* - Transmit control to nearest intercepter, passing object\*
- *Y* -
- *Z* -

### Formats ###

The following formats are going to be supported:

- YAML
- JSON
- XML
- Ruby Pretty Print
- Ruby [Awesome print]()\*
32 changes: 32 additions & 0 deletions lib/letters.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
require "letters/helpers"
require "letters/core_ext"

module Letters
def self.object_for_diff=(object)
@@object = object
end

def self.object_for_diff
@@object if defined?(@@object)
end
end

class Array
include Letters::CoreExt
end

class Hash
include Letters::CoreExt
end

class String
include Letters::CoreExt
end

class NilClass
include Letters::CoreExt
end

# class Object
# include Letters::CoreExt
# end
132 changes: 132 additions & 0 deletions lib/letters/core_ext.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
require "letters/helpers"
require "letters/empty_error"
require "letters/nil_error"

module Letters
module CoreExt
DELIM = '-' * 20

# Beep
def b(opts={})
tap do
$stdout.puts opts[:message] if opts[:message]
$stdout.puts "\a"
end
end

# Callstack
def c(opts={})
tap do
$stdout.puts opts[:message] if opts[:message]
$stdout.puts caller
end
end

# Debug
def d(opts={})
tap do
$stdout.puts opts[:message] if opts[:message]
Helpers.call_debugger
end
end

def d1
tap do |o|
Letters.object_for_diff = o
end
end

def d2(opts={})
require "awesome_print"
opts = { format: "ap" }.merge opts
tap do |o|
diff = Helpers.diff(Letters.object_for_diff, o)
Helpers.out diff, :format => opts[:format]
Letters.object_for_diff = nil
end
end

# Empty check
def e(opts={})
tap do |o|
raise EmptyError if o.empty?
end
end

# File
def f(opts={})
opts = { name: "log", format: "yaml" }.merge opts
tap do |o|
File.open(opts[:name], "w+") do |file|
Helpers.out o, :stream => file, :format => opts[:format]
end
end
end

# Jump
def j(&block)
tap do |o|
o.instance_eval &block
end
end

# Log
def l(opts={})
opts = { level: :info, format: :yaml }.merge opts
tap do |o|
begin
logger.send(opts[:level], opts[:message]) if opts[:message]
logger.send(opts[:level], Helpers.send(opts[:format], o))
rescue
$stdout.puts "[warning] No logger available"
end
end
end

# Nil check
def n(opts={})
tap do |o|
raise NilError if o.nil?
end
end

# Print to STDOUT
def p(opts={})
opts = { format: :ap }.merge opts
tap do |o|
Helpers.out o, :stream => $stdout, :format => opts[:format]
end
end

# RI
def r(opts={})
require "rdoc/ri/driver"
tap do |o|
$stdout.puts opts[:message] if opts[:message]
system "ri #{o.class}"
end
end

# Change safety level
def s(level=nil)
tap do
level ||= $SAFE + 1
Helpers.change_safety level
end
end

# Taint object
def t
tap do |o|
o.taint
end
end

# Untaint object
def u
tap do |o|
o.untaint
end
end
end
end
4 changes: 4 additions & 0 deletions lib/letters/empty_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module Letters
class EmptyError < RuntimeError
end
end
59 changes: 59 additions & 0 deletions lib/letters/helpers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
module Letters
module Helpers
def self.diff(obj1, obj2)
case obj2
when Hash
{
removed: obj1.reject {|k, v| obj2.include? k },
added: obj2.reject {|k, v| obj1.include? k },
updated: obj2.select {|k, v| obj1.include?(k) && obj1[k] != v }
}
when String
diff(obj1.split("\n"), obj2.split("\n"))
else
{
removed: Array(obj1 - obj2),
added: Array(obj2 - obj1)
}
end
rescue
raise "cannot diff the two marked objects"
end

def self.out(object, opts={})
opts = { stream: $stdout, format: :ap }.merge opts
opts[:stream].puts Helpers.send(opts[:format], object)
end

def self.ap(object)
require "awesome_print"
object.awesome_inspect
end

def self.pp(object)
require "pp"
object.pretty_inspect
end

def self.xml(object)
require "xmlsimple"
XmlSimple.xml_out(object, { "KeepRoot" => true })
end

def self.yaml(object)
require "yaml"
object.to_yaml
end

# This provides a mockable method for testing
def self.call_debugger
require "ruby-debug"
debugger
nil
end

def self.change_safety(safety)
$SAFE = safety
end
end
end
4 changes: 4 additions & 0 deletions lib/letters/nil_error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module Letters
class NilError < RuntimeError
end
end
Loading

0 comments on commit 8e8e2c1

Please sign in to comment.