This week I will be reading Redmine's text formatting code. I've worked on Redmine for a few years now but it's text formatting is still a complex mystery to me. The text formatting code is used whenever a rich text area is used; that lets you enter bold, underline, internal Redmine links, etc.

The Code
To display any of this content, Redmine uses a #textilizable method. It uses Textile by default but formatters for markdown, Ruby Doc, reStructedText, Wiki Creole, and plain text.
module ApplicationHelper
# Formats text according to system settings.
# 2 ways to call this method:
# * with a String: textilizable(text, options)
# * with an object and one of its attribute: textilizable(issue, :description, options)
def textilizable(*args)
options = args.last.is_a?(Hash) ? args.pop : {}
case args.size
when 1
obj = options[:object]
text = args.shift
when 2
obj = args.shift
attr = args.shift
text = obj.send(attr).to_s
else
raise ArgumentError, 'invalid arguments to textilizable'
end
return '' if text.blank?
project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
only_path = options.delete(:only_path) == false ? false : true
text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
parse_non_pre_blocks(text) do |text|
[:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
send method_name, text, project, obj, attr, only_path, options
end
end
end
end
Review
#textilizable runs three steps in order to process the content.
- Extracts the options from the args
- Uses the configured formatting engine to convert the content
- Runs the result through some additional methods to convert the Redmine specific formats
Extracts the options from the args
options = args.last.is_a?(Hash) ? args.pop : {}
case args.size
when 1
obj = options[:object]
text = args.shift
when 2
obj = args.shift
attr = args.shift
text = obj.send(attr).to_s
else
raise ArgumentError, 'invalid arguments to textilizable'
end
return '' if text.blank?
project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
only_path = options.delete(:only_path) == false ? false : true
Since #textilizable can be called several different ways, it needs to check how many arguments were passed in. First it removes the final argument so it can use it as an (optional) options hash. This changes the method parameters like:
textilizable(text, options) => textilizable(text)
textilizable(issue, :description, options) => textilizable(issue, :description)
Next the case statement is used to set the text content that needs to be converted. This is done directly in the base case or by calling the method on the object (e.g. textilizable(issue, :description) = > textilizable(issue.description)).
Finally, textilizable sets the project and only_path options from the parameters and object. These options will be used when links are created.
Convert the content using the formatting engine
text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
This single line of code is doing a lot of work to process Redmine's text formatting. The Redmine::WikiFormatting#to_html helper is being used to call the configured formatting, which is configured in Setting#text_formatting. It also looks like #to_html takes a block, which textilizable is using to execute any Redmine wiki macros.
Convert the Redmine specific formats
parse_non_pre_blocks(text) do |text|
[:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
send method_name, text, project, obj, attr, only_path, options
end
end
Finally, #textilizable runs the converted text through a few methods to finish converting Redmine specific markup. Each of these four methods (parse_non_pre_blocks, parse_inline_attachments, parse_wiki_links, parse_redmine_links) are all complex methods so I'll be reading them in more depth this week.
After the #textilizable method runs, it returns an HTML formatting string which can be embedded directly into a view. Tomorrow I'll take a look at Redmine::WikiFormatting#to_html to see how Redmine allows switching the formatting engine.
Tagged: code-reading rails redmine text-formatting
Today I'll be finishing up my code reading session of formtastic by looking at how it creates the input fields.
The Code
module Formtastic #:nodoc:
class SemanticFormBuilder < ActionView::Helpers::FormBuilder
def basic_input_helper(form_helper_method, type, method, options) #:nodoc:
html_options = options.delete(:input_html) || {}
html_options = default_string_options(method, type).merge(html_options) if [:numeric, :string, :password, :text].include?(type)
self.label(method, options_for_label(options)) <<
self.send(form_helper_method, method, html_options)
end
# Outputs a label and standard Rails text field inside the wrapper.
def string_input(method, options)
basic_input_helper(:text_field, :string, method, options)
end
# Outputs a label and standard Rails password field inside the wrapper.
def password_input(method, options)
basic_input_helper(:password_field, :password, method, options)
end
# Outputs a label and standard Rails text field inside the wrapper.
def numeric_input(method, options)
basic_input_helper(:text_field, :numeric, method, options)
end
# Ouputs a label and standard Rails text area inside the wrapper.
def text_input(method, options)
basic_input_helper(:text_area, :text, method, options)
end
# Outputs a label and a standard Rails file field inside the wrapper.
def file_input(method, options)
basic_input_helper(:file_field, :file, method, options)
end
end
end
Review
The first thing I noticed is that many of the common inputs all use the same method to create the input just with different parameters, #basic_input_helper. So lets dig into #basic_input_helper deeper.
HTML Options
html_options = options.delete(:input_html) || {}
html_options = default_string_options(method, type).merge(html_options) if [:numeric, :string, :password, :text].include?(type)
First #basic_input_helper sets up the input_html options. If you remember, this option was used to control the html attributes of the input element in more detail.
Then :numeric, :string, :password, and :text fields will get some extra options from #default_string_options. This includes sizing information that is be pulled directly from the database (e.g. 4 characters max size).
Input generation
self.label(method, options_for_label(options)) <<
self.send(form_helper_method, method, html_options)
Finally, #basic_input_helper generates a label for the input and then runs the ActionView method to create the input. If you remember, the form_helper_method was passed in from the inputs earlier (:text_field, :password_field, etc).
So that completes my code reading of formtastic. I started with the form generation, figured out how it automatically gets all of the fields on a model, walked though how all of the field options are created, saw how it uses reflection to find the type of input for each field, and how those inputs are passed to ActionView to be rendered into HTML. There is still a lot of other code in formtastic, including some generators for more advanced fields if you're interested in reading further.
Tagged: code-reading formtastic forms rails
Today I'm looking into formtastic's SemanticFormBuilder#default_input_type. It's used to guess what type of data is stored in a field so it can show the correct form element.
The Code
module Formtastic #:nodoc:
class SemanticFormBuilder < ActionView::Helpers::FormBuilder
def default_input_type(method, options = {}) #:nodoc:
if column = self.column_for(method)
# Special cases where the column type doesn't map to an input method.
case column.type
when :string
return :password if method.to_s =~ /password/
return :country if method.to_s =~ /country$/
return :time_zone if method.to_s =~ /time_zone/
when :integer
return :select if method.to_s =~ /_id$/
return :numeric
when :float, :decimal
return :numeric
when :timestamp
return :datetime
end
# Try look for hints in options hash. Quite common senario: Enum keys stored as string in the database.
return :select if column.type == :string && options.key?(:collection)
# Try 3: Assume the input name will be the same as the column type (e.g. string_input).
return column.type
else
if @object
return :select if self.reflection_for(method)
file = @object.send(method) if @object.respond_to?(method)
return :file if file && @@file_methods.any? { |m| file.respond_to?(m) }
end
return :select if options.key?(:collection)
return :password if method.to_s =~ /password/
return :string
end
end
end
end
Review
#default_input_type has two main branches for it's checks:
- Is the field a database column?
- Or is the field a virtual column (e.g.
attr_accessor)
This logic for this is handled by ActiveRecord's column_for method and it will either return an ActiveRecord column object or nil.
>> Issue.send(:column_for, :subject)
#<ActiveRecord::ConnectionAdapters::MysqlColumn:0xb5f9abb8 @type=:string, @sql_type="varchar(255)", @precision=nil, @primary=false, @default="", @name="subject", @limit=255, @null=false, @scale=nil>
>> Issue.send(:column_for, :not_a_column)
nil
Database columns
case column.type
when :string
return :password if method.to_s =~ /password/
return :country if method.to_s =~ /country$/
return :time_zone if method.to_s =~ /time_zone/
when :integer
return :select if method.to_s =~ /_id$/
return :numeric
when :float, :decimal
return :numeric
when :timestamp
return :datetime
end
When the method matches a database column, #default_input_type will first use the column type and method name to try to match on a few special cases. For example a :string with 'password' in the method name would become a password field. Integer fields for foreign keys would become selects.
# Try look for hints in options hash. Quite common senario: Enum keys stored as string in the database.
return :select if column.type == :string && options.key?(:collection)
Next #default_input_type checks for enumeration keys that are typically saved to the database as strings.
If none of the previous checks matched, the method will just return the column type.
Virtual Columns
return :select if self.reflection_for(method)
formtastic defines #reflection_for, which uses ActiveRecord's reflect_on_association to see if a method is an association. If so, it returns a select field so you can populate the association directly from the form.
@@file_methods = [ :file?, :public_filename, :filename ]
# ...
file = @object.send(method) if @object.respond_to?(method)
return :file if file && @@file_methods.any? { |m| file.respond_to?(m) }
Here formtastic is doing some checks to see if the method is one that is used to add a file upload. It calls the method to create a new instance of the object and then checks if it responds to any of the @@file_methods (file?, public_filename, filename by default).
return :select if options.key?(:collection)
return :password if method.to_s =~ /password/
If the field isn't for an association or a file upload, formtastic then checks if it should display a select field for a collection or a password field.
If none of the checks have matched, then default_input_type will default to a string field.
Other than the reflections and file upload fields, default_input_type is just doing some pretty straight forward matching. It starts with the most specific matches and slowly broadens out until it returns either the database column type or a string.
Tagged: code-reading formtastic forms rails
Today I'm tackling formtastic's SemanticFormBuilder#input which is used by just about every other part of formtastic to generate the form fields.
The Code
module Formtastic #:nodoc:
class SemanticFormBuilder < ActionView::Helpers::FormBuilder
def input(method, options = {})
if options.key?(:selected) || options.key?(:checked) || options.key?(:default)
::ActiveSupport::Deprecation.warn(
"The :selected, :checked (and :default) options are deprecated in Formtastic and will be removed from 1.0. " <<
"Please set default values in your models (using an after_initialize callback) or in your controller set-up. " <<
"See http://api.rubyonrails.org/classes/ActiveRecord/Callbacks.html for more information.", caller)
end
options[:required] = method_required?(method) unless options.key?(:required)
options[:as] ||= default_input_type(method, options)
html_class = [ options[:as], (options[:required] ? :required : :optional) ]
html_class << 'error' if @object && @object.respond_to?(:errors) && !@object.errors[method.to_sym].blank?
wrapper_html = options.delete(:wrapper_html) || {}
wrapper_html[:id] ||= generate_html_id(method)
wrapper_html[:class] = (html_class << wrapper_html[:class]).flatten.compact.join(' ')
if options[:input_html] && options[:input_html][:id]
options[:label_html] ||= {}
options[:label_html][:for] ||= options[:input_html][:id]
end
input_parts = @@inline_order.dup
input_parts = input_parts - [:errors, :hints] if options[:as] == :hidden
list_item_content = input_parts.map do |type|
send(:"inline_#{type}_for", method, options)
end.compact.join("\n")
return template.content_tag(:li, Formtastic::Util.html_safe(list_item_content), wrapper_html)
end
end
end
Review
Required field
options[:required] = method_required?(method) unless options.key?(:required)
There are two ways to make a field required:
- Set
options[:required] = true in the caller, which will bypass this line of code, or
- Based on the response from
#method_required?. According to #method_required?'s docs it uses the ValidationRefections plugin to automatically check the model for a validation or it checks the all_fields_required_by_default option.
Changing the field type
options[:as] ||= default_input_type(method, options)
formtastic will try to guess which form field to use based on the content, this is what the #default_input_type method is doing here. Since it's using the conditional assignment (||=), the caller can define options[:as] to override the default field.
CSS Classes
html_class = [ options[:as], (options[:required] ? :required : :optional) ]
html_class << 'error' if @object && @object.respond_to?(:errors) && !@object.errors[method.to_sym].blank?
The next section creates an array of css classes to use based on the:
- field type
- if the field is required or optional
- if the object has errors
Wrapper HTML
wrapper_html = options.delete(:wrapper_html) || {}
wrapper_html[:id] ||= generate_html_id(method)
wrapper_html[:class] = (html_class << wrapper_html[:class]).flatten.compact.join(' ')
Here #input is building up an options hash for the li that wraps the field. Pretty standard, nothing too fancy.
Input HTML id
if options[:input_html] && options[:input_html][:id]
options[:label_html] ||= {}
options[:label_html][:for] ||= options[:input_html][:id]
end
Since formtastic lets the caller override the id of the field, it has to be sure that the generated label references the field correctly. I personally have seen a lot of labels in Redmine not referencing the fields correctly, so this little bit of insurance is nice.
Input rendering order
input_parts = @@inline_order.dup
input_parts = input_parts - [:errors, :hints] if options[:as] == :hidden
formtastic lets the application define the order that the different parts of a form input are rendering in. These include:
- the input itself
- text hints about the input
- errors
This section of code copies that this order so it can remove the errors and hints if the field is hidden. This prevents showing help text or the errors on a field that is hidden from the user.
List item content
list_item_content = input_parts.map do |type|
send(:"inline_#{type}_for", method, options)
end.compact.join("\n")
Now that #input has setup all of the options it needs, it iterates over the input_parts and renders each part. For a non-hidden, default ordered field, this would get expanded out to:
parts = []
parts << inline_input_for(method, options)
parts << inline_hints_for(method, options)
parts << inline_errors_for(method, options)
list_item_content = parts.compact.join("\n")
I'll be taking a closer look at #inline_inputs_for later this week but for now it's enough to know that it generates the actual HTML input element.
List item wrapper
return template.content_tag(:li, Formtastic::Util.html_safe(list_item_content), wrapper_html)
Finally, #input wraps the list item content from above in a li. This li is then returned to the #inputs method from yesterday and gets wrapped inside of the form's ul.
formtastic's #input method has shown how formtastic can give developers a bunch of styling and helpers from free. I also touched on how it uses #default_input_type to "guess" what field to render based on the :as symbol. Tomorrow I'll take a look at what #default_input_type uses to guess the field type.
Tagged: code-reading formtastic forms rails
Yesterday I read through formtastic's SemanticFormBuilder#semantic_form_for. Today I'm reading through SemanticFormBuilder#inputs which formtastic uses to create a fieldset and automatically generate all of the form fields needed.
The Code
module Formtastic #:nodoc:
class SemanticFormBuilder < ActionView::Helpers::FormBuilder
def inputs(*args, &block)
title = field_set_title_from_args(*args)
html_options = args.extract_options!
html_options[:class] ||= "inputs"
html_options[:name] = title
if html_options[:for] # Nested form
inputs_for_nested_attributes(*(args << html_options), &block)
elsif block_given?
field_set_and_list_wrapping(*(args << html_options), &block)
else
if @object && args.empty?
args = self.association_columns(:belongs_to)
args += self.content_columns
args -= RESERVED_COLUMNS
args.compact!
end
legend = args.shift if args.first.is_a?(::String)
contents = args.collect { |method| input(method.to_sym) }
args.unshift(legend) if legend.present?
field_set_and_list_wrapping(*((args << html_options) << contents))
end
end
alias :input_field_set :inputs
end
end
Review
The first part of #inputs just sets up options like the title and css classes. The if statement is where formtastic does the form generation:
- When the
:for option is passed, #inputs will build fields for nested attributes using #inputs_for_nested_attributes.
- When a block is passed (like with explicit
input calls) then #field_set_and_list_wrapping is called which generates the block's HTML and wraps it in a fieldset.
- Otherwise,
#inputs will automatically generate form fields for the object based on the model.
Automatic form fields generation
I wanted to take a closer look at how formtastic automatically generates the form fields because this is what attracted me to formtastic in the first place.
if @object && args.empty?
args = self.association_columns(:belongs_to)
args += self.content_columns
args -= RESERVED_COLUMNS
args.compact!
end
legend = args.shift if args.first.is_a?(::String)
contents = args.collect { |method| input(method.to_sym) }
args.unshift(legend) if legend.present?
field_set_and_list_wrapping(*((args << html_options) << contents))
When there is an @object but no args, formtastic builds a list of the @object's columns and of the associated belongs_to models. Then reserved columns like "created_at", "updated_at", and "lock_version" are removed from the list so fields are not created for them (they shouldn't be editable).
legend = args.shift if args.first.is_a?(::String)
Next the legend is generated from the first arg. This confused me at first because the code above overrode the args with an array of the column names and I thought legend would pick up the first column name to use that as the legend. I was wrong though, the column names come across as Symbols so the guard on that line wouldn't run. For example: args are [:title, :description, :name] has no strings.
Next, #inputs runs #input on each column to create the field. This, combined with the column finder above, is how formtastic can automatically find all of the columns from the model that need fields built. So it looks like the #input method will be the next code I read to figure out how the fields are actually created.
Tagged: code-reading formtastic forms rails
I'm starting to read through a new code base this week. In my latest plugin I've integrated formtastic with Redmine, so I decided to start reading through some of formtastic's methods to better understand how it works.
The Code
To start using formtastic, you use the semantic_form_for method so this will be a prime spot to start with.
module Formtastic #:nodoc:
module SemanticFormHelper
[:form_for, :fields_for, :remote_form_for].each do |meth|
module_eval <<-END_SRC, __FILE__, __LINE__ + 1
def semantic_#{meth}(record_or_name_or_array, *args, &proc)
options = args.extract_options!
options[:builder] ||= @@builder
options[:html] ||= {}
class_names = options[:html][:class] ? options[:html][:class].split(" ") : []
class_names << "formtastic"
class_names << case record_or_name_or_array
when String, Symbol then record_or_name_or_array.to_s # :post => "post"
when Array then ActionController::RecordIdentifier.singular_class_name(record_or_name_or_array.last.class) # [@post, @comment] # => "comment"
else ActionController::RecordIdentifier.singular_class_name(record_or_name_or_array.class) # @post => "post"
end
options[:html][:class] = class_names.join(" ")
with_custom_field_error_proc do
#{meth}(record_or_name_or_array, *(args << options), &proc)
end
end
END_SRC
end
end
end
Review
formtastic is using Ruby's module_eval to define semantic_form_for so the first thing I need to do is to evaluate the method into the actual Ruby code.
def semantic_form_for(record_or_name_or_array, *args, &proc)
options = args.extract_options!
options[:builder] ||= @@builder
options[:html] ||= {}
class_names = options[:html][:class] ? options[:html][:class].split(" ") : []
class_names << "formtastic"
class_names << case record_or_name_or_array
when String, Symbol then record_or_name_or_array.to_s # :post => "post"
when Array then ActionController::RecordIdentifier.singular_class_name(record_or_name_or_array.last.class) # [@post, @comment] # => "comment"
else ActionController::RecordIdentifier.singular_class_name(record_or_name_or_array.class) # @post => "post"
end
options[:html][:class] = class_names.join(" ")
with_custom_field_error_proc do
form_for(record_or_name_or_array, *(args << options), &proc)
end
end
Now this code is easier to read.
Options setup
options = args.extract_options!
options[:builder] ||= @@builder
options[:html] ||= {}
The first part of semantic_form_for extracts the options and sets up a few defaults. @@builder is initialized to Formtastic::SemanticFormBuilder but it can be overridden to use another builder class.
HTML class names
class_names = options[:html][:class] ? options[:html][:class].split(" ") : []
class_names << "formtastic"
class_names << case record_or_name_or_array
when String, Symbol then record_or_name_or_array.to_s # :post => "post"
when Array then ActionController::RecordIdentifier.singular_class_name(record_or_name_or_array.last.class) # [@post, @comment] # => "comment"
else ActionController::RecordIdentifier.singular_class_name(record_or_name_or_array.class) # @post => "post"
end
options[:html][:class] = class_names.join(" ")
Next semantic_form_for builds up a set of html classes for the form:
- method options from the caller
- a hardcoded "formtastic"
- the name of the model of the form, based on a symbol or an instance of the model
Rails form_for with custom errors formatting
with_custom_field_error_proc do
form_for(record_or_name_or_array, *(args << options), &proc)
end
Finally semantic_form_for calls Rails' form_for but it replaces it replaces how the error messages are shown by wrapping the method in #with_custom_error_proc.
# Override the default ActiveRecordHelper behaviour of wrapping the input.
# This gets taken care of semantically by adding an error class to the LI tag
# containing the input.
#
FIELD_ERROR_PROC = proc do |html_tag, instance_tag|
html_tag
end
def with_custom_field_error_proc(&block)
@@default_field_error_proc = ::ActionView::Base.field_error_proc
::ActionView::Base.field_error_proc = FIELD_ERROR_PROC
result = yield
::ActionView::Base.field_error_proc = @@default_field_error_proc
result
end
What it's doing is to:
- change
ActionView::Base.field_error_proc to use the custom FIELD_ERROR_PROC object
- yields the control (which calls
form_for)
- then replaces the
ActionView::Base.field_error_proc
This is done so semantic_form_for can control how the errors are displayed without over-ridding the non-formtastic forms.
Now that I understand how formtastic's form builder is setup, I can start reading through the field generation methods.
Tagged: code-reading formtastic forms rails
Since yesterday I reviewed the RestClient::Response class, it's time to read the parent class RestClient::AbstractResponse. There are a few interesting methods in AbstractResponse but the #return! method is a good one to review.
The Code
module RestClient
class AbstractResponse
# Return the default behavior corresponding to the response code:
# the response itself for code in 200..206, redirection for 301 and 302 in get and head cases, redirection for 303 and an exception in other cases
def return! &block
if (200..206).include? code
self
elsif [301, 302].include? code
unless [:get, :head].include? args[:method]
raise Exceptions::EXCEPTIONS_MAP[code], self
else
follow_redirection &block
end
elsif code == 303
args[:method] = :get
args.delete :payload
follow_redirection &block
elsif Exceptions::EXCEPTIONS_MAP[code]
raise Exceptions::EXCEPTIONS_MAP[code], self
else
raise RequestFailed, self
end
end
end
end
Review
There is a lot of branches here based on the HTTP status code, so I'm going to take it one branch at a time.
200 - 206 Success
if (200..206).include? code
self
The success status codes will just return self, the Response object.
301 or 302 - Redirect
elsif [301, 302].include? code
unless [:get, :head].include? args[:method]
raise Exceptions::EXCEPTIONS_MAP[code], self
else
follow_redirection &block
end
301 and 302 responses both have two different responses.
- If the request was using HTTP GET or HTTP HEAD, RestClient will automatically follow the redirect.
- Otherwise it will raise an exception with the message '301 - Moved Permanently' or '302 - Found'.
303 - See Other
elsif code == 303
args[:method] = :get
args.delete :payload
follow_redirection &block
The HTTP 303 code is sent so the client will retry their request at a new URI using HTTP GET. RestClient accomplishes this by changing the :method, deleting the POST/PUT :payload, and following the redirection.
Other status codes handled by RestClient
elsif Exceptions::EXCEPTIONS_MAP[code]
raise Exceptions::EXCEPTIONS_MAP[code], self
RestClient handles other status codes by throwing an exception. This happens when the request was successful at the transmission level, but it was rejected by the server. The status codes RestClient supports is defined in exceptions.rb.
STATUSES = {100 => 'Continue',
101 => 'Switching Protocols',
200 => 'OK',
201 => 'Created',
202 => 'Accepted',
203 => 'Non-Authoritative Information',
204 => 'No Content',
205 => 'Reset Content',
206 => 'Partial Content',
300 => 'Multiple Choices',
301 => 'Moved Permanently',
302 => 'Found',
303 => 'See Other',
304 => 'Not Modified',
305 => 'Use Proxy',
400 => 'Bad Request',
401 => 'Unauthorized',
403 => 'Forbidden',
404 => 'Resource Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
408 => 'Request Timeout',
409 => 'Conflict',
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
413 => 'Request Entity Too Large',
414 => 'Request-URI Too Long',
415 => 'Unsupported Media Type',
416 => 'Requested Range Not Satisfiable',
417 => 'Expectation Failed',
500 => 'Internal Server Error',
501 => 'Not Implemented',
502 => 'Bad Gateway',
503 => 'Service Unavailable',
504 => 'Gateway Timeout',
505 => 'HTTP Version Not Supported'}
Other status codes not handled by RestClient
else
raise RequestFailed, self
end
Finally, if there is a status code that isn't handled by RestClient, a RequestFailed is thrown. This would happen for status 207 (Multi-Status, used in WebDAV) or 307 (Temporary Redirect, HTTP/1.1).
RestClient::AbstractResponse#return! highlights an important rule when writing multiple if/else or case statements: put the most common cases first. I learned this from Code Complete several years ago and use it every day when writing code. It makes it easier to separate the main and edge case branches of the code.
Tagged: code-reading RestClient http apis
Yesterday I looked at the Request side of RestClient so I'm reading through the RestClient::Response class today. RestClient::Request#execute returns this response object from it's #process_result.
response = Response.new(Request.decode(res['content-encoding'], res.body), res, args)
The Code
module RestClient
# A Response from RestClient, you can access the response body, the code or the headers.
#
class Response < AbstractResponse
attr_reader :body
WARNING_MESSAGE = '[warning] The Response is no more a String and the Response content is now accessed through Response.body, please update your code'
def initialize body, net_http_res, args
super net_http_res, args
@body = body || ""
end
def method_missing symbol, *args
if body.respond_to? symbol
warn WARNING_MESSAGE
body.send symbol, *args
else
super
end
end
def == o
if super
true
else
equal_body = (body == o)
if equal_body
warn WARNING_MESSAGE
end
equal_body
end
end
def to_s
body.to_s
end
def size
body.size
end
end
end
RestClient::Response is a pretty small class because there is a lot of common logic shared in it's parent class RestClient::AbstractResponse.
RestClient::Response#initialize
def initialize body, net_http_res, args
super net_http_res, args
@body = body || ""
end
#initialize isn't doing very much here, just calling it's parent class (AbstractResponse) and initializing the @body attribute.
RestClient::Response#method_missing
def method_missing symbol, *args
if body.respond_to? symbol
warn WARNING_MESSAGE
body.send symbol, *args
else
super
end
end
This #method_missing is setting up a proxy to the body. Based on the warning message here, I think RestClient used to return a string but now it's returning the Response object instead. Using #method_missing this way is interesting, it keeps backwards compatibility and gives other developers time to update their own code.
RestClient::Response#==
def == o
if super
true
else
equal_body = (body == o)
if equal_body
warn WARNING_MESSAGE
end
equal_body
end
end
This comparison method is doing two things. First it's checking if the two objects are the same object (using super). If that fails, it checks if the body contents are equal; if so it warns about the backwards compatibility (above) and then returns the results of that comparison.
RestClient::Response#to_s and RestClient::Response#size
def to_s
body.to_s
end
def size
body.size
end
Both of these methods are just delegated to the body object. At first I thought the #method_missing call would handle #to_s but then I remembered that all Ruby objects respond to #to_s. This would prevent #method_missing from being called and Response would just return a string like:
#<RestClient::Response:0xb6f9e5dc>
Tomorrow I'll look into AbstractResponse class, since that's where a lot of the processing logic for a Response is at.
Tagged: code-reading RestClient http apis

I've just released the first version of Redmine Simple Support. It is a Redmine plugin that makes it easy to link Redmine issues to external resources like a third party bug tracker or support system.
Download
The plugin can be download from the Little Stream Software project or from GitHub.
Features
- Enter multiple support urls on each issue
- Basic text substitution for support ticket ids (Example)
- Issue filter for searching for support urls on the issues list
- Permissions to control viewing or editing support urls
Help
If you need help, my Redmine bug tracker is open to the public and you are welcome to ask for help there.
Thanks
I would like to thank Luke Kanies from Puppet Labs for sponsoring this plugin. If you find it useful, send your appreciation his way.
Tagged: support plugin rails redmine ruby
I've just released Redmine Kanban 0.2.0. This release includes fixes for eight bugs and 19 new features. This version requires Redmine 0.9, which is the current stable version of Redmine.
Download
The plugin can be download from the Little Stream Software project or from GitHub.
Changes
Help
If you need help, my Redmine bug tracker is open to the public and you are welcome to ask for help there.
Eric
Tagged: kanban plugin rails redmine ruby