Skip to content

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also .

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also .
  • 5 commits
  • 28 files changed
  • 0 commit comments
  • 1 contributor
Commits on Apr 15, 2012
Philip (flip) Kromer nuke_constants test helper. refactored capture_output / dummy_stdio c4d8956
Philip (flip) Kromer separated inflections into inflector module + string mixin 8e5b3d5
Philip (flip) Kromer methods from Avdi Grimm's confident code 0b111cc
Philip (flip) Kromer methods on standard exceptions to raise a copycat error (eg to raise …
…a 'wrong number of arguments (#{args.length} for #{allowed_arity})' ArgumentError)
9f8b14a
Philip (flip) Kromer iterating on model definition; still in rough form af388ae
View
1 Gemfile
@@ -9,6 +9,7 @@ group :development do
gem 'jeweler', "~> 1.6"
gem 'rspec', "~> 2.5"
gem 'yard', "~> 0.6"
+ gem 'pry'
end
group :docs do
View
3 Guardfile
@@ -5,7 +5,8 @@
# watch(%r{notes/.+\.(md|txt)}) { "notes" }
# end
-rspec_opts = '--format progress' # or 'doc' for more verbose
+# '--format doc' for more verbose
+rspec_opts = '--format progress ' # --tag model_spec'
guard 'rspec', :version => 2, :cli => rspec_opts do
watch(%r{^spec/.+_spec\.rb$})
View
17 lib/gorillib/exception/confidence.rb
@@ -0,0 +1,17 @@
+module Kernel
+ def assert(value, message="Assertion failed", error=StandardError)
+ raise error, message, caller unless value
+ end
+end
+
+class NullObject
+ def method_missing(*args, &block)
+ self
+ end
+
+ def nil?; true; end
+end
+
+def Maybe(value)
+ value.nil? ? NullObject.new : value
+end
View
44 lib/gorillib/exception/raisers.rb
@@ -0,0 +1,44 @@
+Exception.class_eval do
+ def self.caller_parts
+ p caller[1]
+ mg = %r{\A([^:]+):(\d+):in \`([^\']+)\'\z}.match(caller[1]) or return [caller[1], 1, 'unknown']
+ [mg[1], mg[2].to_i, mg[3]]
+ end
+end
+
+ArgumentError.class_eval do
+ #
+ #
+ # @example `*args` distinguishes between no args and a nil arg, but we have to handle error ourselves
+ # define_method(field_name) do |*args|
+ # raise ArgumentError.wrong_number(*args.length, 1) unless *
+ # end
+ #
+ def self.wrong_number(expected, got)
+
+ end
+
+ def self.check_arity!(args, allowed_arity)
+ allowed_arity = (0 .. allowed_arity) if allowed_arity.is_a?(Fixnum)
+ unless allowed_arity.include?(args.length)
+ self.new("wrong number of arguments (#{args.length} for #{allowed_arity})")
+ end
+ true
+ end
+end
+
+
+NoMethodError.class_eval do
+ MESSAGE = "undefined method `%s' for %s:%s"
+
+ def self.undefined_method(obj)
+ file, line, meth = caller_parts
+ self.new(MESSAGE % [meth, obj, obj.class])
+ end
+
+ def self.abstract(obj)
+ file, line, meth = caller_parts
+ self.new("#{MESSAGE} -- must be implemented by the subclass" % [meth, obj, obj.class])
+ end
+
+end
View
30 lib/gorillib/io/system_helpers.rb
@@ -0,0 +1,30 @@
+module Gorillib::CheckedPopen
+ module_function
+
+ def checked_popen(command, mode, fail_action, io_class=IO)
+ check_child_exit_status do
+ io_class.popen(command, mode) do |process|
+ yield(process)
+ end
+ end
+ rescue Errno::EPIPE
+ fail_action.call
+ end
+
+ # @private
+ NO_EXIT_STATUS = OpenStruct.new(:exitstatus => 0)
+
+ def check_child_exit_status
+ result = yield
+ status = $? || NO_EXIT_STATUS
+ unless [0, 172].include?(status.exitstatus)
+ raise ArgumentError, "Command exited with status '#{status.exitstatus}'"
+ end
+ result
+ end
+
+end
+
+::IO.class_eval do
+ include Gorillib::CheckedPopen
+end
View
3 lib/gorillib/model.rb
@@ -1,7 +1,4 @@
require "gorillib/metaprogramming/concern"
-require "gorillib/object/try_dup"
-
-require "gorillib/model/attributes"
module Gorillib
#
View
1 lib/gorillib/model/README.md
@@ -13,7 +13,6 @@
A model class has fields
A model instance has attributes that correspond to those fields
-
`.receive` .new, #receive!
`#receive!` `receive_attribute` on each attribute in the hash; `receive_remaining` on the leftovers
`#receive_attribute` type converts val, calls `write_attribute`
View
5 lib/gorillib/model/TODO.md
@@ -0,0 +1,5 @@
+
+
+* figure out the method structure for
+ - read/write/unset of attributes when Hash vs Accessors vs Instance Variables
+ - reader/writer: raw vs. hooks, dirty, etc.
View
68 lib/gorillib/model/active_model_conversion.rb
@@ -0,0 +1,68 @@
+module Gorillib::Model
+ # == Active Model Conversions
+ #
+ # Handles default conversions: to_model, to_key, to_param, and to_partial_path.
+ #
+ # Let's take for example this non-persisted object.
+ #
+ # class ContactMessage
+ # include ActiveModel::Conversion
+ #
+ # # ContactMessage are never persisted in the DB
+ # def persisted?
+ # false
+ # end
+ # end
+ #
+ # cm = ContactMessage.new
+ # cm.to_model == self # => true
+ # cm.to_key # => nil
+ # cm.to_param # => nil
+ # cm.to_path # => "contact_messages/contact_message"
+ #
+ module Conversion
+ extend Gorillib::Concern
+
+ # If your object is already designed to implement all of the Active Model
+ # you can use the default <tt>:to_model</tt> implementation, which simply
+ # returns self.
+ #
+ # If your model does not act like an Active Model object, then you should
+ # define <tt>:to_model</tt> yourself returning a proxy object that wraps
+ # your object with Active Model compliant methods.
+ def to_model
+ self
+ end
+
+ # Returns an Enumerable of all key attributes if any is set, regardless
+ # if the object is persisted or not.
+ #
+ # Note the default implementation uses persisted? just because all objects
+ # in Ruby 1.8.x responds to <tt>:id</tt>.
+ def to_key
+ persisted? ? [id] : nil
+ end
+
+ # Returns a string representing the object's key suitable for use in URLs,
+ # or nil if <tt>persisted?</tt> is false.
+ def to_param
+ persisted? ? to_key.join('-') : nil
+ end
+
+ # Returns a string identifying the path associated with the object.
+ # ActionPack uses this to find a suitable partial to represent the object.
+ def to_partial_path
+ self.class._to_partial_path
+ end
+
+ module ClassMethods #:nodoc:
+ # Provide a class level cache for the to_path. This is an
+ # internal method and should not be accessed directly.
+ def _to_partial_path #:nodoc:
+ @_to_partial_path ||= begin
+ "#{model_name.collection}/#{model_name.element}".freeze
+ end
+ end
+ end
+ end
+end
View
87 lib/gorillib/model/active_model_naming.rb
@@ -0,0 +1,87 @@
+module Gorillib::Model
+ class Name < String
+ attr_accessor :namespace
+ attr_reader :singular, :element, :collection, :partial_path, :param_key, :i18n_key
+
+ alias_method :cache_key, :collection
+
+ class_attribute :inflector
+ self.inflector ||= Gorillib::String::Inflector
+
+ def initialize(klass, namespace = nil, name = nil)
+ name ||= klass.name
+ raise ArgumentError, "Class name cannot be blank. You need to supply a name argument when anonymous class given" if name.blank?
+
+ super(name)
+
+ @klass = klass
+ @singular = _singularize(self).freeze
+ @element = inflector.underscore(inflector.demodulize(self)).freeze
+ @human = inflector.humanize(@element).freeze
+ @i18n_key = inflector.underscore(self).to_sym
+ #
+ self.namespace = namespace
+ self.plural = inflector.pluralize(@singular)
+ end
+
+ def plural=(str)
+ @plural = str.dup.freeze
+ @collection = inflector.underscore(@plural).freeze
+ @partial_path = "#{@collection}/#{@element}".freeze
+ str
+ end
+
+ def namespace=(ns)
+ if ns.present?
+ @unnamespaced = self.sub(/^#{ns.name}::/, '')
+ @param_key = _singularize(@unnamespaced).freeze
+ else
+ @unnamespaced = nil
+ @param_key = @singular.freeze
+ end
+ end
+
+ def singular_route_key
+ inflector.singularize(route_key).freeze
+ end
+
+ def route_key
+ rk = (namespace ? inflector.pluralize(param_key) : plural.dup)
+ rk << "_index" if plural == singular
+ rk.freeze
+ end
+
+ private
+
+ def _singularize(string, replacement='_')
+ inflector.underscore(string).tr('/', replacement)
+ end
+ end
+
+ # == Active Model Naming
+ #
+ # Creates a +model_name+ method on your object.
+ #
+ # To implement, just extend ActiveModel::Naming in your object:
+ #
+ # class BookCover
+ # extend ActiveModel::Naming
+ # end
+ #
+ # BookCover.model_name # => "BookCover"
+ # BookCover.model_name.human # => "Book cover"
+ #
+ # BookCover.model_name.i18n_key # => :book_cover
+ # BookModule::BookCover.model_name.i18n_key # => :"book_module/book_cover"
+ #
+ # Providing the functionality that ActiveModel::Naming provides in your object
+ # is required to pass the Active Model Lint test. So either extending the provided
+ # method below, or rolling your own is required.
+ module Naming
+ # Returns a Name object for module, which can be used to retrieve all kinds
+ # of naming-related information.
+ def model_name
+ @_model_name ||= Gorillib::Model::Name.new(self, namespace)
+ end
+ end
+end
View
13 lib/gorillib/model/active_model_shim.rb
@@ -13,14 +13,23 @@ module Model
module ActiveModelShim
extend Gorillib::Concern
extend ActiveModel::Naming
- include ActiveModel::Conversion
+ include Gorillib::Model::Conversion
include ActiveModel::Validations
# @return [false]
def persisted?
false
end
- end
+
+ # Overrides ActiveModel::AttributeMethods
+ # @private
+ def attribute_method?(attr_name)
+ self.class.has_field?(attr_name)
+ end
+
+ module ClassMethods
+ end # ActiveModelShim::ClassMethods
+ end # ActiveModelShim
end
end
View
7 lib/gorillib/model/dsl_field.rb
@@ -0,0 +1,7 @@
+module Gorillib
+
+ module Model
+ module Configurable
+ end
+ end
+end
View
28 lib/gorillib/model/field.rb
@@ -23,7 +23,7 @@ def initialize(name, type, model, hsh={})
@model = model
@name = name.to_sym
@type = type
- @options = Mash.new
+ @options = {}
receive!(hsh)
end
@@ -33,6 +33,22 @@ def receive!(hsh)
@default = @options[:default]
end
+ def doc
+ @options[:doc] || "#{name} attribute"
+ end
+
+ INSCRIBED_METHOD_TYPES = [:read, :write, :unset]
+
+ def visibility(meth_type)
+ raise ArgumentError, "method type must be one of #{INSCRIBED_METHOD_TYPES.join(', ')}" unless INSCRIBED_METHOD_TYPES.include?(meth_type)
+ case @options[meth_type]
+ when true then :public
+ when nil then :public
+ when false then :none
+ else @options[meth_type]
+ end
+ end
+
# Compare field definitions
#
# @example
@@ -52,6 +68,10 @@ def [](key)
@options[key.to_sym]
end
+ def to_hash
+ @options
+ end
+
# The field name
# @return [String] the field name
def to_s
@@ -74,8 +94,8 @@ def to_sym
#
# @since 0.6.0
def inspect
- args = [name.inspect, type.to_s, to_hash.map{|key, val| "#{key.inspect} => #{val.inspect}" }.sort]
- "field #{args.join(", ")}"
+ args = [name.inspect, type.to_s, to_hash.map{|key, val| "#{key.inspect} => #{val.inspect}" }].reject(&:blank?)
+ "field(#{args.join(", ")})"
end
protected
@@ -85,7 +105,7 @@ def inspect
module Valid
VALID_NAME_RE = /\A[A-Za-z_][A-Za-z0-9_]+\z/
- def validate_name!(name)
+ def self.validate_name!(name)
raise TypeError, "can't convert #{name.class} into Symbol" unless name.respond_to? :to_sym
raise ArgumentError, "Name must start with [A-Za-z_] and subsequently contain only [A-Za-z0-9_]" unless name =~ VALID_NAME_RE
end
View
68 lib/gorillib/model/named_type.rb
@@ -0,0 +1,68 @@
+module Meta
+ module Schema
+
+ #
+ # Provides
+ #
+ module NamedSchema
+
+ #
+ # Returns the metamodel -- a module extending the type, on which all the
+ # model methods are inscribed. This allows you to override the model methods
+ # and call +super()+ to get the generic behavior.
+ #
+ # The metamodel is named for the including class, but with 'Meta::'
+ # prepended and 'Type' appended -- so Geo::Place has metamodel
+ # "Meta::Geo::PlaceType"
+ #
+ def metamodel
+ return @metamodel if @metamodel
+ @metamodel = Meta::Schema::NamedSchema.get_nested_module("Meta::#{self.name}Type")
+ self.class_eval{ include(@metamodel) }
+ @metamodel
+ end
+
+ protected
+
+ ALLOWED_VISIBILITIES = [:public, :private, :protected].freeze
+
+ # OPTIMIZE: apparently `define_method(:foo){ ... }` is slower than `def foo() ... end`
+ def define_metamodel_method(method_name, visibility=:public, &block)
+ return if visibility == :none || visibility == false
+ raise ArgumentError, "Visibility must be one of #{ ALLOWED_VISIBILITIES.join(', ') }, got '#{ visibility }'" unless ALLOWED_VISIBILITIES.include?(visibility)
+ instance_method_already_implemented?(method_name)
+ metamodel.module_eval{ define_method(method_name, &block) }
+ metamodel.module_eval "#{visibility} :#{method_name}", __FILE__, __LINE__
+ end
+
+ # These methods are deprecated on the Object class and so can be safely overridden
+ DEPRECATED_OBJECT_METHODS = %w[ id type ]
+
+ # Overrides ActiveModel::AttributeMethods
+ # @private
+ def instance_method_already_implemented?(method_name)
+ deprecated_object_method = DEPRECATED_OBJECT_METHODS.include?(method_name.to_s)
+ already_implemented = (not deprecated_object_method) && self.allocate.respond_to?(method_name, true)
+ raise ::Gorillib::Model::DangerousFieldError, "A field named '#{method_name}' would conflict with an existing method" if already_implemented
+ false
+ end
+
+ # Returns a module for the given names, rooted at Object (so
+ # implicity with '::').
+ # @example
+ # get_nested_module(["This", "That", "TheOther"])
+ # # This::That::TheOther
+ def self.get_nested_module(name)
+ name.split('::').inject(Object) do |parent_module, module_name|
+ # inherit = false makes these methods be scoped to parent_module instead of universally
+ if parent_module.const_defined?(module_name, false)
+ parent_module.const_get(module_name, false)
+ else
+ parent_module.const_set(module_name.to_sym, Module.new)
+ end
+ end
+ end
+
+ end
+ end
+end
View
207 lib/gorillib/model/record_type.rb
@@ -1,37 +1,24 @@
-module Gorillib
- module RecordType
+module Meta
+ module Type
# Provides a set of class methods for defining a field schema and instance
# methods for reading and writing attributes.
#
# @example Usage
# class Person
# include Gorillib::Meta::RecordType
- # field :name, String, :doc => 'Full name of person'
+ #
+ # field :name, String, :doc => 'Full name of person'
+ # field :height, Float, :doc => 'Height in meters'
# end
#
- # person = Person.new
+ # person = Person.new
# person.name = "Bob Dobbs, Jr"
+ # puts person #=> #<Person name="Bob Dobbs, Jr">
#
- module Attributes
- extend ActiveSupport::Concern
+ module RecordType
- # Two records are equal if they have the same class and their attributes
- # are equal.
- #
- # @example Compare for equality.
- # model == other
- #
- # @param [ActiveAttr::Attributes, Object] other The other model to compare
- #
- # @return [true, false] True if attributes are equal and other is instance
- # of the same Class, false if not.
- #
- # @since 0.2.0
- def ==(other)
- return false unless other.instance_of? self.class
- attributes == other.attributes
- end
+ extend Gorillib::Concern
# Returns a Hash of all attributes
#
@@ -40,82 +27,101 @@ def ==(other)
#
# @return [Hash{Symbol => Object}] The Hash of all attributes
#
- def attributes
- Hash[ self.class.field_names.map{|key| [key, read_attribute(key)] } ]
- end
- alias_method :to_hash, :attributes
-
- # Returns the class name plus its attributes
- #
- # @return [String] Human-readable presentation of the attributes
+ # FIXME: should this include unset
#
- def inspect
- str = "#<" << self.class.name
- str << " " unless attribute_descriptions.empty?
- str << attributes.map{|attr, val| "#{attr}: #{value.inspect}" }.join(", ")
- str << ">"
- str
+ def attributes
+ self.class.field_names.inject({}) do |hsh, fn|
+ hsh[fn] = read_attribute(fn) if attribute_set?(fn) ; hsh
+ end
end
# Read a value from the model's attributes.
#
- # @example Read an attribute with read_attribute
+ # @example Reading an attribute
# person.read_attribute(:name)
- # @example Read an attribute with bracket syntax
- # person[:name]
#
- # @param [String, Symbol, #to_s] name The name of the attribute to get.
+ # @param [String, Symbol, #to_s] fn The name of the attribute to get.
#
# @return [Object] The value of the attribute.
#
# @raise [UnknownAttributeError] if the attribute is unknown
#
- def read_attribute(name)
- if self.class.has_field?(name)
- send(name.to_s)
+ def read_attribute(fn)
+ if self.class.has_field?(fn)
+ send(fn.to_s)
else
- raise UnknownFieldError, "unknown field: #{name}"
+ raise UnknownFieldError, "unknown field: #{fn}"
end
end
- alias_method :[], :read_attribute
- # Write the value of a single attribute
+ # Write the value of a single attribute.
#
- # @example Write the attribute with write_attribute
+ # @example Writing an attribute
# person.write_attribute(:name, "Benjamin")
- # @example Write an attribute with bracket syntax
- # person[:name] = "Benjamin"
#
- # @param [String, Symbol, #to_s] name The name of the attribute to update.
+ # @param [String, Symbol, #to_s] fn The fn of the attribute to update.
# @param [Object] value The value to set for the attribute.
#
# @raise [UnknownAttributeError] if the attribute is unknown
#
- # @since 0.2.0
- def write_attribute(name, value)
- if self.class.has_field?(name)
- send("#{name}=", value)
+ def write_attribute(fn, value)
+ if self.class.has_field?(fn)
+ send("#{fn}=", value)
else
- raise UnknownAttributeError, "unknown attribute: #{name}"
+ raise UnknownAttributeError, "unknown attribute: #{fn}"
end
end
- alias_method :[]=, :write_attribute
- protected
+ def unset_attribute(fn)
+ write_attribute(fn, nil)
+ end
- # Overrides ActiveModel::AttributeMethods
- # @private
- def attribute_method?(attr_name)
- self.class.has_field?(attr_name)
+ def attribute_set?
+ true
end
- module ClassMethods
- included do
- class_attribute :fields unless self.respond_to?(:fields)
- self.fields ||= Hash.new
+ def attribute_default(fn)
+ val = case field.default
+ case val
+ when nil then nil
+ when Proc then (val.arity == 0) ? instance_exec(&val) : val.call(self, fn)
+ else field.default
end
+ end
- # Defines an field
+ # Two records are equal if they have the same class and their attributes
+ # are equal.
+ #
+ # @example Compare for equality.
+ # model == other
+ #
+ # @param [ActiveAttr::Attributes, Object] other The other model to compare
+ #
+ # @return [true, false] True if attributes are equal and other is instance
+ # of the same Class, false if not.
+ #
+ def ==(other)
+ return false unless other.instance_of?(self.class)
+ attributes == other.attributes
+ end
+
+ # Returns the class name plus its attributes
+ #
+ # @return [String] Human-readable presentation of the attributes
+ #
+ def inspect
+ str = "#<" << self.class.name
+ str << " " unless attribute_descriptions.empty?
+ str << attributes.map{|attr, val| "#{attr}: #{value.inspect}" }.join(", ")
+ str << ">"
+ str
+ end
+
+ protected
+
+ module ClassMethods
+
+ # Defines a new field
#
# For each field that is defined, a getter and setter will be added as
# an instance method to the model. An Field instance will be added to
@@ -124,21 +130,33 @@ module ClassMethods
# @example
# field :height, Integer
#
- # @param [Symbol] name -- The field name. Must start with `[A-Za-z_]` and subsequently contain only `[A-Za-z0-9_]`
+ # @param [Symbol] fn -- The field name. Must start with `[A-Za-z_]` and subsequently contain only `[A-Za-z0-9_]`
# @param [Class] type -- The field's type (required)
# @option options [String] doc -- Documentation string for the field (optional)
# @option options [Proc, Object] default -- Default value, or proc that instance can evaluate to find default value
#
- # @raise [DangerousAttributeError] if the attribute name conflicts with
+ # @macro [attach] property
+ # @return [$2] the $1 property ($3)
+ #
+ # @raise [DangerousAttributeError] if the field name conflicts with
# existing methods
#
- def field(name, type, options={})
- field_def = Gorillib::Model::Field.new(name, type, options)
- fields[field_def.name] = field_def
+ def field(fn, type, options={})
+ field_def = ::Gorillib::Model::Field.new(fn, type, options)
+ @_fields[field_def.name] = field_def
define_field_methods(field_def)
field_def
end
+ def fields
+ fields = {}
+ self.ancestors.reverse.each do |ancestor|
+ next unless ancestor.instance_variable_defined?('@_fields')
+ fields.merge! ancestor.instance_variable_get('@_fields')
+ end
+ fields
+ end
+
# Array of field names as Symbols
#
# @return [Array<String>] The attribute names
@@ -154,53 +172,28 @@ def inspect
"#{self.name}[#{ field_names.join(", ") }]"
end
- # def meta_module
- # "Meta::Type::#{self.name}Type"
- # end
- #
- # def meta_module_method(name, &block)
- # end
- #
- # def define_field_methods(field)
- # meta_module_method("receive_#{field}") do
- #
- # end
- # meta_module.module_eval{ attr_accessor(field.name) }
- # end
-
- # def add_field_accessor(field_name, schema)
- # visibility = schema[:accessor] || :public
- # reader_meth = field_name ; writer_meth = "#{field_name}=" ; attr_name = "@#{field_name}"
- # unless (visibility == :none)
- # define_metamodel_method(reader_meth, visibility){ instance_variable_get(attr_name) }
- # define_metamodel_method(writer_meth, visibility){|v| instance_variable_set(attr_name, v) }
- # end
- # end
-
protected
- # Methods deprecated on the Object class which can be safely overridden
- DEPRECATED_OBJECT_METHODS = %w[ id type ]
-
- # Overrides ActiveModel::AttributeMethods
- # @private
- def instance_method_already_implemented?(method_name)
- deprecated_object_method = DEPRECATED_OBJECT_METHODS.include?(method_name.to_s)
- already_implemented = !deprecated_object_method && self.allocate.respond_to?(method_name, true)
- raise DangerousAttributeError, %Q{An attribute method named "#{method_name}" would conflict with an existing method} if already_implemented
- false
+ def define_field_methods(field)
+ ivar_name = "@#{field.name}"
+ define_metamodel_method(field.name, field.visibility(:read )){ instance_variable_get(ivar_name) }
+ define_metamodel_method("#{field.name}=", field.visibility(:write)){|v| instance_variable_set(ivar_name, v) }
+ define_metamodel_method("unset_#{field.name}", field.visibility(:unset)){ remove_instance_variable(ivar_name) }
end
private
# assign fields to subclasses
- #
- # FIXME: can't add fields to superclass after subclass was made
def inherited(subclass)
super
- subclass.fields = fields.dup
+ subclass.instance_variable_set('@_fields', {})
end
end
+
+ included do
+ extend Meta::Schema::NamedSchema
+ @_fields = {}
+ end
end
end
View
265 lib/gorillib/string/bad_inflector.rb
@@ -0,0 +1,265 @@
+# String inflections define new methods on the String class to transform names for different purposes.
+#
+# "ScaleScore".underscore # => "scale_score"
+#
+# This doesn't define the full set of inflections -- only
+#
+# * camelize
+# * snakeize
+# * underscore
+# * demodulize
+#
+module Gorillib::String::Inflector
+ extend self
+
+ def self.pluralizations
+ @pluralizations ||= {}
+ end
+
+ def pluralize(str)
+ Gorillib::String::Inflector.pluralizations.fetch(str){ "#{str}s" }
+ end
+
+ # The reverse of +pluralize+, returns the singular form of a word in a string.
+ #
+ # Examples:
+ # "posts".singularize # => "post"
+ # # it's not very smart
+ # "octopi".singularize # => "octopi"
+ # "bonus".singularize # => "bonu"
+ # "boxes".singularize # => "boxe"
+ # "CamelOctopi".singularize # => "CamelOctopi"
+ def singularize(word)
+ Gorillib::String::Inflector.pluralizations.reverse.fetch(str){ str.gsub(/s$/, '') }
+ end
+
+ # Capitalizes the first word and turns underscores into spaces and strips a
+ # trailing "_id", if any. Like +titleize+, this is meant for creating pretty output.
+ #
+ # Examples:
+ # "employee_salary" # => "Employee salary"
+ # "author_id" # => "Author"
+ def humanize(lower_case_and_underscored_word)
+ result = lower_case_and_underscored_word.to_s.dup
+ result.gsub!(/_id$/, "")
+ result.gsub(/(_)?([a-z\d]*)/i){ "#{ $1 && ' ' }#{ $2.downcase}" }.gsub(/^\w/){ $&.upcase }
+ end
+
+ # Capitalizes all the words and replaces some characters in the string to create
+ # a nicer looking title. +titleize+ is meant for creating pretty output. It is not
+ # used in the Rails internals.
+ #
+ # +titleize+ is also aliased as as +titlecase+.
+ #
+ # Examples:
+ # "man from the boondocks".titleize # => "Man From The Boondocks"
+ # "x-men: the last stand".titleize # => "X Men: The Last Stand"
+ # "TheManWithoutAPast".titleize # => "The Man Without A Past"
+ # "raiders_of_the_lost_ark".titleize # => "Raiders Of The Lost Ark"
+ def titleize(word)
+ humanize(underscore(word)).gsub(/\b('?[a-z])/){ $1.capitalize }
+ end
+
+ # Create the name of a table like Rails does for models to table names. This method
+ # uses the +pluralize+ method on the last word in the string.
+ #
+ # Examples
+ # "RawScaledScorer".tableize # => "raw_scaled_scorers"
+ # "egg_and_ham".tableize # => "egg_and_hams"
+ # "fancyCategory".tableize # => "fancy_categories"
+ def tableize(class_name)
+ pluralize(underscore(class_name))
+ end
+
+ # Create a class name from a plural table name like Rails does for table names to models.
+ # Note that this returns a string and not a Class. (To convert to an actual class
+ # follow +classify+ with +constantize+.)
+ #
+ # Examples:
+ # "egg_and_hams".classify # => "EggAndHam"
+ # "posts".classify # => "Post"
+ #
+ # Singular names are not handled correctly:
+ # "business".classify # => "Busines"
+ def classify(table_name)
+ # strip out any leading schema name
+ camelize(singularize(table_name.to_s.sub(/.*\./, '')))
+ end
+
+private
+
+ # def uncountable_words #:doc
+ # %w( equipment information rice money species series fish )
+ # end
+ #
+ # def plural_rules #:doc:
+ # [
+ # [/(x|ch|ss|sh)$/i, '\1es'], # search, switch, fix, box, process, address
+ # [/([^aeiouy]|qu)y$/i, '\1ies'], # query, ability, agency
+ # [/(p)erson$/i, '\1eople'], # person, salesperson
+ # [/(m)an$/i, '\1en'], # man, woman, spokesman
+ # [/(c)hild$/i, '\1hildren'], # child
+ # [/s$/i, 's'], # no change (compatibility)
+ # [/$/, 's']
+ # ]
+ # end
+ #
+ # def singular_rules
+ # [
+ # [/(x|ch|ss|sh)es$/i, '\1'],
+ # [/([^aeiouy]|qu)ies$/i, '\1y'],
+ # [/(p)eople$/i, '\1\2erson'],
+ # [/(m)en$/i, '\1an'],
+ # [/(c)hildren$/i, '\1\2hild'],
+ # [/s$/i, '']
+ # ]
+ # end
+
+ # %w[
+ # equipment equipment
+ # information information
+ # rice rice
+ # money money
+ # species species
+ # series series
+ # fish fish
+ # ]
+
+ # def uncountable_words #:doc
+ # %w( equipment information rice money species series fish )
+ # end
+ #
+ # def plural_rules #:doc:
+ # [
+ # [/^(ox)$/i, '\1\2en'], # ox
+ # [/([m|l])ouse$/i, '\1ice'], # mouse, louse
+ # [/(matr|vert|ind)ix|ex$/i, '\1ices'], # matrix, vertex, index
+ # [/(x|ch|ss|sh)$/i, '\1es'], # search, switch, fix, box, process, address
+ # [/([^aeiouy]|qu)ies$/i, '\1y'],
+ # [/([^aeiouy]|qu)y$/i, '\1ies'], # query, ability, agency
+ # [/(hive)$/i, '\1s'], # archive, hive
+ # [/(?:([^f])fe|([lr])f)$/i, '\1\2ves'], # half, safe, wife
+ # [/sis$/i, 'ses'], # basis, diagnosis
+ # [/([ti])um$/i, '\1a'], # datum, medium
+ # [/(p)erson$/i, '\1eople'], # person, salesperson
+ # [/(m)an$/i, '\1en'], # man, woman, spokesman
+ # [/(c)hild$/i, '\1hildren'], # child
+ # [/(buffal|tomat)o$/i, '\1\2oes'], # buffalo, tomato
+ # [/(bu)s$/i, '\1\2ses'], # bus
+ # [/(alias)/i, '\1es'], # alias
+ # [/(octop|vir)us$/i, '\1i'], # octopus, virus - virus has no defined plural (according to Latin/dictionary.com), but viri is better than viruses/viruss
+ # [/(ax|cri|test)is$/i, '\1es'], # axis, crisis
+ # [/s$/i, 's'], # no change (compatibility)
+ # [/$/, 's']
+ # ]
+ # end
+ #
+ # def singular_rules
+ # [
+ # [/(matr)ices$/i, '\1ix'],
+ # [/(vert|ind)ices$/i, '\1ex'],
+ # [/^(ox)en/i, '\1'],
+ # [/(alias)es$/i, '\1'],
+ # [/([octop|vir])i$/i, '\1us'],
+ # [/(cris|ax|test)es$/i, '\1is'],
+ # [/(shoe)s$/i, '\1'],
+ # [/(o)es$/i, '\1'],
+ # [/(bus)es$/i, '\1'],
+ # [/([m|l])ice$/i, '\1ouse'],
+ # [/(x|ch|ss|sh)es$/i, '\1'],
+ # [/(m)ovies$/i, '\1\2ovie'],
+ # [/(s)eries$/i, '\1\2eries'],
+ # [/([^aeiouy]|qu)ies$/i, '\1y'],
+ # [/([lr])ves$/i, '\1f'],
+ # [/(tive)s$/i, '\1'],
+ # [/(hive)s$/i, '\1'],
+ # [/([^f])ves$/i, '\1fe'],
+ # [/([ti])a$/i, '\1um'],
+ # [/(p)eople$/i, '\1\2erson'],
+ # [/(m)en$/i, '\1an'],
+ # [/(s)tatus$/i, '\1\2tatus'],
+ # [/(c)hildren$/i, '\1\2hild'],
+ # [/(n)ews$/i, '\1\2ews'],
+ # [/s$/i, '']
+ # ]
+ # end
+
+ # inflect.plural(/$/, 's')
+ # inflect.plural(/s$/i, 's')
+ # inflect.plural(/^(ax|test)is$/i, '\1es')
+ # inflect.plural(/(octop|vir)us$/i, '\1i')
+ # inflect.plural(/(octop|vir)i$/i, '\1i')
+ # inflect.plural(/(alias|status)$/i, '\1es')
+ # inflect.plural(/(bu)s$/i, '\1ses')
+ # inflect.plural(/(buffal|tomat)o$/i, '\1oes')
+ # inflect.plural(/([ti])um$/i, '\1a')
+ # inflect.plural(/([ti])a$/i, '\1a')
+ # inflect.plural(/sis$/i, 'ses')
+ # inflect.plural(/(?:([^f])fe|([lr])f)$/i, '\1\2ves')
+ # inflect.plural(/(hive)$/i, '\1s')
+ # inflect.plural(/([^aeiouy]|qu)y$/i, '\1ies')
+ # inflect.plural(/(x|ch|ss|sh)$/i, '\1es')
+ # inflect.plural(/(matr|vert|ind)(?:ix|ex)$/i, '\1ices')
+ # inflect.plural(/(m|l)ouse$/i, '\1ice')
+ # inflect.plural(/(m|l)ice$/i, '\1ice')
+ # inflect.plural(/^(ox)$/i, '\1en')
+ # inflect.plural(/^(oxen)$/i, '\1')
+ # inflect.plural(/(quiz)$/i, '\1zes')
+
+ # inflect.singular(/s$/i, '')
+ # inflect.singular(/(ss)$/i, '\1')
+ # inflect.singular(/(n)ews$/i, '\1ews')
+ # inflect.singular(/([ti])a$/i, '\1um')
+ # inflect.singular(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/i, '\1\2sis')
+ # inflect.singular(/(^analy)(sis|ses)$/i, '\1sis')
+ # inflect.singular(/([^f])ves$/i, '\1fe')
+ # inflect.singular(/(hive)s$/i, '\1')
+ # inflect.singular(/(tive)s$/i, '\1')
+ # inflect.singular(/([lr])ves$/i, '\1f')
+ # inflect.singular(/([^aeiouy]|qu)ies$/i, '\1y')
+ # inflect.singular(/(s)eries$/i, '\1eries')
+ # inflect.singular(/(m)ovies$/i, '\1ovie')
+ # inflect.singular(/(x|ch|ss|sh)es$/i, '\1')
+ # inflect.singular(/(m|l)ice$/i, '\1ouse')
+ # inflect.singular(/(bus)(es)?$/i, '\1')
+ # inflect.singular(/(o)es$/i, '\1')
+ # inflect.singular(/(shoe)s$/i, '\1')
+ # inflect.singular(/(cris|test)(is|es)$/i, '\1is')
+ # inflect.singular(/^(a)x[ie]s$/i, '\1xis')
+ # inflect.singular(/(octop|vir)(us|i)$/i, '\1us')
+ # inflect.singular(/(alias|status)(es)?$/i, '\1')
+ # inflect.singular(/^(ox)en/i, '\1')
+ # inflect.singular(/(vert|ind)ices$/i, '\1ex')
+ # inflect.singular(/(matr)ices$/i, '\1ix')
+ # inflect.singular(/(quiz)zes$/i, '\1')
+ # inflect.singular(/(database)s$/i, '\1')
+ #
+ # inflect.irregular('person', 'people')
+ # inflect.irregular('man', 'men')
+ # inflect.irregular('child', 'children')
+ # inflect.irregular('sex', 'sexes')
+ # inflect.irregular('move', 'moves')
+ # inflect.irregular('cow', 'kine')
+ # inflect.irregular('zombie', 'zombies')
+ #
+ # inflect.uncountable(%w(equipment information rice money species series fish sheep jeans))
+
+public
+
+ # def pluralize(word)
+ # result = word.dup
+ # plural_rules.each do |(rule, replacement)|
+ # break if result.gsub!(rule, replacement)
+ # end
+ # return result
+ # end
+ #
+ # def singularize(word)
+ # result = word.dup
+ # singular_rules.each do |(rule, replacement)|
+ # break if result.gsub!(rule, replacement)
+ # end
+ # return result
+ # end
+
+end
View
35 lib/gorillib/string/constantize.rb
@@ -1,21 +1,28 @@
+require 'gorillib/string/inflector'
class String
- # Constantize tries to find a declared constant with the name specified
+ # +constantize+ tries to find a declared constant with the name specified
# in the string. It raises a NameError when the name is not in CamelCase
- # or is not initialized.
- #
- # @example
- # "Module".constantize #=> Module
- # "Class".constantize #=> Class
- #
- # This is the extlib version of String#constantize, which has different
- # behavior wrt using lexical context: see active_support/inflector/methods.rb
+ # or is not initialized. See Gorillib::Model::Inflector.constantize
#
+ # Examples
+ # "Module".constantize # => Module
+ # "Class".constantize # => Class
+ # "blargle".constantize # => NameError: wrong constant name blargle
def constantize
- unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ self
- raise NameError, "#{self.inspect} is not a valid constant name!"
- end
-
- Object.module_eval("::#{$1}", __FILE__, __LINE__)
+ Gorillib::Model::Inflector.constantize(self)
end unless method_defined?(:constantize)
+
+ # +safe_constantize+ tries to find a declared constant with the name specified
+ # in the string. It returns nil when the name is not in CamelCase
+ # or is not initialized. See Gorillib::Model::Inflector.safe_constantize
+ #
+ # Examples
+ # "Module".safe_constantize # => Module
+ # "Class".safe_constantize # => Class
+ # "blargle".safe_constantize # => nil
+ def safe_constantize
+ Gorillib::Model::Inflector.safe_constantize(self)
+ end unless method_defined?(:safe_constantize)
+
end
View
81 lib/gorillib/string/inflections.rb
@@ -1,78 +1,7 @@
-# String inflections define new methods on the String class to transform names for different purposes.
-#
-# "ScaleScore".underscore # => "scale_score"
-#
-# This doesn't define the full set of inflections -- only
-#
-# * camelize
-# * snakeize
-# * underscore
-# * demodulize
-#
-class String
-
- # By default, +camelize+ converts strings to UpperCamelCase. If the argument to +camelize+
- # is set to <tt>:lower</tt> then +camelize+ produces lowerCamelCase.
- #
- # +camelize+ will also convert '/' to '::' which is useful for converting paths to namespaces.
- #
- # @example:
- # "active_record".camelize # => "ActiveRecord"
- # "active_record".camelize(:lower) # => "activeRecord"
- # "active_record/errors".camelize # => "ActiveRecord::Errors"
- # "active_record/errors".camelize(:lower) # => "activeRecord::Errors"
- #
- # As a rule of thumb you can think of +camelize+ as the inverse of +underscore+,
- # though there are cases where that does not hold:
- #
- # "SSLError".underscore.camelize # => "SslError"
- #
- def camelize(first_letter = :upper)
- camelized = self.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
- (first_letter == :lower) ? (self[0..0].downcase + camelized[1..-1]) : camelized
- end unless method_defined?(:camelize)
-
- # Converts strings to snakeCase, also known as lowerCamelCase.
- #
- # +snakeize+ will also convert '/' to '::' which is useful for converting paths to namespaces.
- #
- # @example:
- # "active_record".snakeize # => "activeRecord"
- # "active_record/errors".snakeize # => "activeRecord::Errors"
- #
- def snakeize
- camelize :lower
- end unless method_defined?(:snakeize)
-
- # Makes an underscored, lowercase form from the expression in the string.
- #
- # +underscore+ will also change '::' to '/' to convert namespaces to paths.
- #
- # Examples:
- # "ActiveRecord".underscore # => "active_record"
- # "ActiveRecord::Errors".underscore # => active_record/errors
- #
- # As a rule of thumb you can think of +underscore+ as the inverse of +camelize+,
- # though there are cases where that does not hold:
- #
- # "SSLError".underscore.camelize # => "SslError"
- def underscore
- word = self.dup
- word.gsub!(/::/, '/')
- word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
- word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
- word.tr!("-", "_")
- word.downcase!
- word
- end unless method_defined?(:underscore)
-
- # Removes the module part from the expression in the string
- #
- # @example
- # "ActiveRecord::CoreExtensions::String::Inflections".demodulize #=> "Inflections"
- # "Inflections".demodulize #=> "Inflections"
- def demodulize
- self.gsub(/^.*::/, '')
- end unless method_defined?(:demodulize)
+class String
+ def camelize(*args) Gorillib::String::Inflections.camelize(self, *args) ; end
+ def snakeize(*args) Gorillib::String::Inflections.snakeize(self, *args) ; end
+ def underscore(*args) Gorillib::String::Inflections.underscore(self, *args) ; end
+ def demodulize(*args) Gorillib::String::Inflections.demodulize(self, *args) ; end
end
View
190 lib/gorillib/string/inflector.rb
@@ -0,0 +1,190 @@
+# String inflections define new methods on the String class to transform names for different purposes.
+#
+# "ScaleScore".underscore # => "scale_score"
+#
+# This doesn't define the full set of inflections -- only
+#
+# * camelize
+# * snakeize
+# * underscore
+# * demodulize
+#
+module Gorillib::String::Inflector
+ extend self
+
+ # By default, +camelize+ converts strings to UpperCamelCase. If the argument to +camelize+
+ # is set to <tt>:lower</tt> then +camelize+ produces lowerCamelCase.
+ #
+ # +camelize+ will also convert '/' to '::' which is useful for converting paths to namespaces.
+ #
+ # @example:
+ # "active_record".camelize # => "ActiveRecord"
+ # "active_record".camelize(:lower) # => "activeRecord"
+ # "active_record/errors".camelize # => "ActiveRecord::Errors"
+ # "active_record/errors".camelize(:lower) # => "activeRecord::Errors"
+ #
+ # As a rule of thumb you can think of +camelize+ as the inverse of +underscore+,
+ # though there are cases where that does not hold:
+ #
+ # "SSLError".underscore.camelize # => "SslError"
+ #
+ def camelize(str, first_letter = :upper)
+ camelized = str.gsub(/\/(.?)/){ "::#{ $1.upcase }" }.gsub(/(?:^|_)(.)/){ $1.upcase }
+ (first_letter == :lower) ? (str[0..0].downcase + camelized[1..-1]) : camelized
+ end
+
+ # Converts strings to snakeCase, also known as lowerCamelCase.
+ #
+ # +snakeize+ will also convert '/' to '::' which is useful for converting paths to namespaces.
+ #
+ # @example:
+ # "active_record".snakeize # => "activeRecord"
+ # "active_record/errors".snakeize # => "activeRecord::Errors"
+ #
+ def snakeize(str)
+ camelize(str, :lower)
+ end
+
+ # Makes an underscored, lowercase form from the expression in the string.
+ #
+ # +underscore+ will also change '::' to '/' to convert namespaces to paths.
+ #
+ # Examples:
+ # "ActiveRecord".underscore # => "active_record"
+ # "ActiveRecord::Errors".underscore # => active_record/errors
+ #
+ # As a rule of thumb you can think of +underscore+ as the inverse of +camelize+,
+ # though there are cases where that does not hold:
+ #
+ # "SSLError".underscore.camelize # => "SslError"
+ def underscore(str)
+ word = str.dup
+ word.gsub!(/::/, '/')
+ word.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
+ word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
+ word.tr!("-", "_")
+ word.downcase!
+ word
+ end
+
+ # Replaces underscores with dashes in the string.
+ #
+ # Example:
+ # "puni_puni" # => "puni-puni"
+ def dasherize(underscored_word)
+ underscored_word.gsub(/_/, '-')
+ end
+
+ # Removes the module part from the expression in the string:
+ #
+ # @example
+ # "Gorillib::String::Inflections".demodulize #=> "Inflections"
+ # "Inflections".demodulize #=> "Inflections"
+ #
+ # See also +deconstantize+.
+ def demodulize(str)
+ str.gsub(/^.*::/, '')
+ end
+
+ # Removes the rightmost segment from the constant expression in the string:
+ #
+ # "Net::HTTP".deconstantize # => "Net"
+ # "::Net::HTTP".deconstantize # => "::Net"
+ # "String".deconstantize # => ""
+ # "::String".deconstantize # => ""
+ # "".deconstantize # => ""
+ #
+ # See also +demodulize+.
+ def deconstantize(path)
+ path.to_s[0...(path.rindex('::') || 0)] # implementation based on the one in facets' Module#spacename
+ end
+
+ # Constantize tries to find a declared constant with the name specified
+ # in the string. It raises a NameError when the name is not in CamelCase
+ # or is not initialized.
+ #
+ # @example
+ # "Module".constantize #=> Module
+ # "Class".constantize #=> Class
+ #
+ # This is the extlib version of String#constantize, which has different
+ # behavior wrt using lexical context: see active_support/inflector/methods.rb
+ #
+ def constantize(str)
+ unless /\A(?:::)?([A-Z]\w*(?:::[A-Z]\w*)*)\z/ =~ str
+ raise NameError, "#{self.inspect} is not a valid constant name!"
+ end
+
+ Object.module_eval("::#{$1}", __FILE__, __LINE__)
+ end
+
+ # Tries to find a constant with the name specified in the argument string:
+ #
+ # "Module".safe_constantize # => Module
+ # "Test::Unit".safe_constantize # => Test::Unit
+ #
+ # The name is assumed to be the one of a top-level constant, no matter whether
+ # it starts with "::" or not. No lexical context is taken into account:
+ #
+ # C = 'outside'
+ # module M
+ # C = 'inside'
+ # C # => 'inside'
+ # "C".safe_constantize # => 'outside', same as ::C
+ # end
+ #
+ # nil is returned when the name is not in CamelCase or the constant (or part of it) is
+ # unknown.
+ #
+ # "blargle".safe_constantize # => nil
+ # "UnknownModule".safe_constantize # => nil
+ # "UnknownModule::Foo::Bar".safe_constantize # => nil
+ #
+ def safe_constantize(camel_cased_word)
+ begin
+ constantize(camel_cased_word)
+ rescue NameError => e
+ raise unless e.message =~ /uninitialized constant #{const_regexp(camel_cased_word)}$/ ||
+ e.name.to_s == camel_cased_word.to_s
+ rescue ArgumentError => e
+ raise unless e.message =~ /not missing constant #{const_regexp(camel_cased_word)}\!$/
+ end
+ end
+
+ # Turns a number into an ordinal string used to denote the position in an
+ # ordered sequence such as 1st, 2nd, 3rd, 4th.
+ #
+ # Examples:
+ # ordinalize(1) # => "1st"
+ # ordinalize(2) # => "2nd"
+ # ordinalize(1002) # => "1002nd"
+ # ordinalize(1003) # => "1003rd"
+ # ordinalize(-11) # => "-11th"
+ # ordinalize(-1021) # => "-1021st"
+ def ordinalize(number)
+ if (11..13).include?(number.to_i.abs % 100)
+ "#{number}th"
+ else
+ case number.to_i.abs % 10
+ when 1; "#{number}st"
+ when 2; "#{number}nd"
+ when 3; "#{number}rd"
+ else "#{number}th"
+ end
+ end
+ end
+
+private
+
+ # Mount a regular expression that will match part by part of the constant.
+ # For instance, Foo::Bar::Baz will generate Foo(::Bar(::Baz)?)?
+ def const_regexp(camel_cased_word) #:nodoc:
+ parts = camel_cased_word.split("::")
+ last = parts.pop
+
+ parts.reverse.inject(last) do |acc, part|
+ part.empty? ? acc : "#{part}(::#{acc})?"
+ end
+ end
+
+end
View
30 lib/gorillib/test_helpers/capture_output.rb
@@ -2,27 +2,25 @@ module Gorillib
module TestHelpers
module_function
+ def dummy_stdio(stdin_text=nil)
+ stdin = stdin_text.nil? ? $stdin : StringIO.new(stdin_text)
+ new_fhs = [stdin, StringIO.new('', 'w'), StringIO.new('', 'w')]
+ old_fhs = [$stdin, $stdout, $stderr]
+ begin
+ $stdin, $stdout, $stderr = new_fhs
+ yield
+ ensure
+ $stdin, $stdout, $stderr = old_fhs
+ end
+ new_fhs[1..2]
+ end
+
#
# Temporarily sets the global variables $stderr and $stdout to a capturable StringIO;
# restores them at the end, even if there is an error
#
def capture_output
- local_stdout = StringIO.new('', 'w')
- local_stderr = StringIO.new('', 'w')
-
- begin
- old_stdout = $stdout ; $stdout = local_stdout
- old_stderr = $stderr ; $stderr = local_stderr
-
- yield
-
- $stdout = old_stdout
- $stderr = old_stderr
- return [local_stdout, local_stderr]
- ensure
- $stdout = old_stdout
- $stderr = old_stderr
- end
+ dummy_stdio{ yield }
end
alias_method :quiet_output, :capture_output
View
9 lib/gorillib/test_helpers/nuke_constants.rb
@@ -0,0 +1,9 @@
+class Module
+ #
+ # Removes all constants in the module's namespace -- this is useful when
+ # writing specs for metaprogramming methods
+ #
+ def nuke_constants
+ constants.each{|const| remove_const(const) }
+ end
+end
2 notes
@@ -1 +1 @@
-Subproject commit 6a208d4ee012bb1d2f35334baaff3de010661bbc
+Subproject commit f316c9c4c3206388976f9a06275626876e730633
View
83 spec/gorillib/model/record_type_spec.rb
@@ -0,0 +1,83 @@
+require File.expand_path('../../spec_helper', File.dirname(__FILE__))
+#
+require 'gorillib/metaprogramming/concern'
+require 'gorillib/metaprogramming/remove_method'
+require "gorillib/object/try_dup"
+#
+require 'gorillib/model/errors'
+require 'gorillib/model/field'
+require 'gorillib/model/named_type'
+require 'gorillib/model/record_type'
+#
+require 'model_test_helpers'
+
+require 'pry'
+
+describe Gorillib::Model, :model_spec => true do
+ context '.field' do
+ end
+
+ context '.fields' do
+ it 'has no fields by default' do
+ poppa_smurf.fields.should == {}
+ end
+
+ it 'inherits parent fields' do
+ poppa_smurf.field :height, Integer
+ smurfette.field :pulchritude, Float
+ poppa_smurf.fields.keys.should == [ :height ]
+ smurfette.fields.keys.should == [ :height, :pulchritude ]
+ end
+
+ it 'raises an error if a field is redefined' do
+ poppa_smurf.field :height, Integer
+ poppa_smurf.send(:define_method, :boogie){ 'na na na' }
+ ->{ smurfette.field :height, Float }.should raise_error(::Gorillib::Model::DangerousFieldError, /A field named 'height'.*conflict/)
+ ->{ smurfette.field :boogie, Float }.should raise_error(::Gorillib::Model::DangerousFieldError, /A field named 'boogie'.*conflict/)
+ end
+ end
+
+end
+
+describe Meta::Schema::NamedSchema, :model_spec => true do
+
+ context '.metamodel' do
+ it 'defines a new module named Meta::[KlassName]Type' do
+ defined?(::Meta::Gorillib::Scratch::PoppaSmurfType).should be_false
+ poppa_smurf.metamodel.should == ::Meta::Gorillib::Scratch::PoppaSmurfType
+ end
+ it 'includes metamodule in host class' do
+ poppa_smurf.metamodel
+ poppa_smurf.should < ::Meta::Gorillib::Scratch::PoppaSmurfType
+ end
+ end
+
+ context '#define_metamodel_method' do
+ before{ Meta::Schema::NamedSchema.send(:public, :define_metamodel_method) }
+
+ it 'adds method to metamodel' do
+ poppa_smurf.define_metamodel_method(:smurf){ 'smurfy!' }
+ poppa_smurf.public_instance_methods.should include(:smurf)
+ poppa_smurf.metamodel.public_instance_methods.should include(:smurf)
+ poppa_smurf.new.smurf.should == 'smurfy!'
+ end
+
+ context 'visibility' do
+ it 'raises an error if an illegal visibility is given' do
+ poppa_smurf.define_metamodel_method(:smurf, :public){ 'public' }
+ poppa_smurf.public_instance_methods.should include(:smurf)
+ end
+ it 'raises an error if an illegal visibility is given' do
+ poppa_smurf.define_metamodel_method(:smurf, :private){ 'private' }
+ poppa_smurf.private_instance_methods.should include(:smurf)
+ end
+ it 'raises an error if an illegal visibility is given' do
+ poppa_smurf.define_metamodel_method(:smurf, :protected){ 'protected' }
+ poppa_smurf.protected_instance_methods.should include(:smurf)
+ end
+ it 'raises an error if an illegal visibility is given' do
+ ->{ poppa_smurf.define_metamodel_method(:smurf, :smurfily){ } }.should raise_error(ArgumentError, /^Visibility must be.*'smurfily'/)
+ end
+ end
+ end
+end
View
2 spec/gorillib/model_spec.rb
@@ -1,4 +1,4 @@
-require File.expand_path('../../spec_helper', File.dirname(__FILE__))
+require File.expand_path('../spec_helper', File.dirname(__FILE__))
require 'gorillib/model'
describe Gorillib::Model, :model_spec => true do
View
9 spec/gorillib/test_helpers/capture_output_spec.rb
@@ -68,13 +68,4 @@
end
- it 'makes module_functions' do
- klass = Class.new
- klass.private_method_defined?(:capture_output).should be_false
- klass.private_method_defined?(:quiet_output ).should be_false
- klass.send(:include, Gorillib::TestHelpers)
- klass.private_method_defined?(:capture_output).should be_true
- klass.private_method_defined?(:quiet_output ).should be_true
- end
-
end
View
9 spec/spec_helper.rb
@@ -11,11 +11,14 @@ def GORILLIB_ROOT_DIR *paths
# Spork.prefork do # Must restart for changes to config / code from libraries loaded here
$LOAD_PATH.unshift(GORILLIB_ROOT_DIR('lib'))
- $LOAD_PATH.unshift(GORILLIB_ROOT_DIR('spec/support'))
+$LOAD_PATH.unshift(GORILLIB_ROOT_DIR('spec/support'))
+require 'gorillib_test_helpers'
Dir[GORILLIB_ROOT_DIR('spec/support/matchers/*.rb')].each {|f| require f}
- RSpec.configure do |config|
- end
+
+RSpec.configure do |config|
+ include Gorillib::TestHelpers
+end
# end
# Spork.each_run do # This code will be run each time you run your specs.
View
5 spec/support/gorillib_test_helpers.rb
@@ -0,0 +1,5 @@
+require 'gorillib/test_helpers/capture_output'
+require 'gorillib/test_helpers/nuke_constants'
+
+module Gorillib::TestHelpers
+end
View
12 spec/support/model_test_helpers.rb
@@ -0,0 +1,12 @@
+module ::Gorillib::Scratch ; end
+module ::Meta ; module Gorillib ; module Scratch ; end ; end ; end
+
+shared_context 'model', :model_spec => true do
+ let(:poppa_smurf ){ Gorillib::Scratch::PoppaSmurf = Class.new{ include Meta::Type::RecordType } }
+ let(:smurfette ){ Gorillib::Scratch::Smurfette = Class.new(poppa_smurf) }
+
+ after do
+ ::Gorillib::Scratch.nuke_constants
+ ::Meta::Gorillib::Scratch.nuke_constants
+ end
+end

No commit comments for this range

Something went wrong with that request. Please try again.