You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@usergrid.apache.org by sn...@apache.org on 2016/10/24 13:06:00 UTC

[15/83] [abbrv] usergrid git commit: Moving older SDKs to a difference location and updating main README to link to new SDK locations.

http://git-wip-us.apache.org/repos/asf/usergrid/blob/867060fa/sdks/other/ruby-on-rails/lib/usergrid_ironhorse/query.rb
----------------------------------------------------------------------
diff --git a/sdks/other/ruby-on-rails/lib/usergrid_ironhorse/query.rb b/sdks/other/ruby-on-rails/lib/usergrid_ironhorse/query.rb
new file mode 100644
index 0000000..6562b8f
--- /dev/null
+++ b/sdks/other/ruby-on-rails/lib/usergrid_ironhorse/query.rb
@@ -0,0 +1,929 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# http://guides.rubyonrails.org/active_record_querying.html
+
+module Usergrid
+  module Ironhorse
+
+    class Query
+
+      RecordNotFound = ActiveRecord::RecordNotFound
+
+      def initialize(model_class)
+        @model_class = model_class
+        @options = {}
+      end
+
+      ## Initializes new record from relation while maintaining the current
+      ## scope.
+      ##
+      ## Expects arguments in the same format as +Base.new+.
+      ##
+      ##   users = User.where(name: 'DHH')
+      ##   user = users.new # => #<User id: nil, name: "DHH", created_at: nil, updated_at: nil>
+      ##
+      ## You can also pass a block to new with the new record as argument:
+      ##
+      ##   user = users.new { |user| user.name = 'Oscar' }
+      ##   user.name # => Oscar
+      #def new(*args, &block)
+      #  scoping { @model_class.new(*args, &block) }
+      #end
+
+      # Find by uuid or name - This can either be a specific uuid or name (1), a list of uuids
+      # or names (1, 5, 6), or an array of uuids or names ([5, 6, 10]).
+      # If no record can be found for all of the listed ids, then RecordNotFound will be raised.
+      #
+      #   Person.find(1)       # returns the object for ID = 1
+      #   Person.find("1")     # returns the object for ID = 1
+      #   Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
+      #   Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
+      #   Person.find([1])     # returns an array for the object with ID = 1
+      #   Person.where("administrator = 1").order("created_on DESC").find(1)
+      #
+      def find(*ids)
+        raise RecordNotFound unless ids
+        ids = ids.first if ids.first.is_a? Array
+        @records = ids.collect { |id| find_one! id } # todo: can this be optimized in one call?
+        #entities = @model_class.resource[ids.join '&'].get.entities
+        #raise RecordNotFound unless (entities.size == ids.size)
+        #@records = entities.collect {|entity| @model_class.model_name.constantize.new(entity.data) }
+        @records.size == 1 ? @records.first : @records
+      end
+
+      # Finds the first record matching the specified conditions. There
+      # is no implied ordering so if order matters, you should specify it
+      # yourself.
+      #
+      # If no record is found, returns <tt>nil</tt>.
+      #
+      #   Post.find_by name: 'Spartacus', rating: 4
+      #   Post.find_by "published_at < ?", 2.weeks.ago
+      def find_by(*conditions)
+        where(*conditions).take
+      end
+
+      # Like <tt>find_by</tt>, except that if no record is found, raises
+      # an <tt>ActiveRecord::RecordNotFound</tt> error.
+      def find_by!(*conditions)
+        where(*conditions).take!
+      end
+
+      # Gives a record (or N records if a parameter is supplied) without any implied
+      # order.
+      #
+      #   Person.take # returns an object fetched by SELECT * FROM people
+      #   Person.take(5) # returns 5 objects fetched by SELECT * FROM people LIMIT 5
+      #   Person.where(["name LIKE '%?'", name]).take
+      def take(limit=1)
+        limit(limit).to_a
+      end
+
+      # Same as +take+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+      # is found. Note that <tt>take!</tt> accepts no arguments.
+      def take!
+        take or raise RecordNotFound
+      end
+
+      # Find the first record (or first N records if a parameter is supplied).
+      # If no order is defined it will order by primary key.
+      #
+      #   Person.first # returns the first object fetched by SELECT * FROM people
+      #   Person.where(["user_name = ?", user_name]).first
+      #   Person.where(["user_name = :u", { :u => user_name }]).first
+      #   Person.order("created_on DESC").offset(5).first
+      #   Person.first(3) # returns the first three objects fetched by SELECT * FROM people LIMIT 3
+      def first(limit=1)
+        limit(limit).load.first
+      end
+
+      # Same as +first+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+      # is found. Note that <tt>first!</tt> accepts no arguments.
+      def first!
+        first or raise RecordNotFound
+      end
+
+      # Find the last record (or last N records if a parameter is supplied).
+      # If no order is defined it will order by primary key.
+      #
+      #   Person.last # returns the last object fetched by SELECT * FROM people
+      #   Person.where(["user_name = ?", user_name]).last
+      #   Person.order("created_on DESC").offset(5).last
+      #   Person.last(3) # returns the last three objects fetched by SELECT * FROM people.
+      #
+      # Take note that in that last case, the results are sorted in ascending order:
+      #
+      #   [#<Person id:2>, #<Person id:3>, #<Person id:4>]
+      #
+      # and not:
+      #
+      #   [#<Person id:4>, #<Person id:3>, #<Person id:2>]
+      def last(limit=1)
+        limit(limit).reverse_order.load.first
+      end
+
+      # Same as +last+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+      # is found. Note that <tt>last!</tt> accepts no arguments.
+      def last!
+        last or raise RecordNotFound
+      end
+
+      def each
+        to_a.each { |*block_args| yield(*block_args) }
+        while @response.data['cursor'] && !limit_value
+          next_page
+          to_a.each { |*block_args| yield(*block_args) }
+        end
+      end
+
+      def next_page
+        @options[:cursor] = @response.data['cursor']
+        @records = nil
+        load
+        self
+      end
+
+      # Returns +true+ if a record exists in the table that matches the +id+ or
+      # conditions given, or +false+ otherwise. The argument can take six forms:
+      #
+      # * String - Finds the record with a primary key corresponding to this
+      #   string (such as <tt>'5'</tt>).
+      # * Array - Finds the record that matches these +find+-style conditions
+      #   (such as <tt>['color = ?', 'red']</tt>).
+      # * Hash - Finds the record that matches these +find+-style conditions
+      #   (such as <tt>{color: 'red'}</tt>).
+      # * +false+ - Returns always +false+.
+      # * No args - Returns +false+ if the table is empty, +true+ otherwise.
+      #
+      # For more information about specifying conditions as a Hash or Array,
+      # see the Conditions section in the introduction to ActiveRecord::Base.
+      def exists?(conditions=nil)
+        # todo: does not yet handle all conditions described above
+        case conditions
+          when Array, Hash
+            pluck :uuid
+            !where(conditions).take.empty?
+          else
+            !!find_one(conditions)
+        end
+      end
+      alias_method :any?, :exists?
+      alias_method :many?, :exists?
+
+      def limit(limit=1)
+        @options[:limit] = limit
+        self
+      end
+
+      def offset(num)
+        @options[:offset] = num
+        self
+      end
+
+      # Removes from the query the condition(s) specified in +skips+.
+      #
+      # Example:
+      #
+      #   Post.order('id asc').except(:order)                  # discards the order condition
+      #   Post.where('id > 10').order('id asc').except(:where) # discards the where condition but keeps the order
+      #
+      def except(*skips)
+        skips.each {|option| @options.delete option}
+      end
+
+      # Removes any condition from the query other than the one(s) specified in +onlies+.
+      #
+      # Example:
+      #
+      #   Post.order('id asc').only(:where)         # discards the order condition
+      #   Post.order('id asc').only(:where, :order) # uses the specified order
+      #
+      def only(*onlies)
+        @options.keys do |k|
+          unless onlines.include? k
+            @options.delete k
+          end
+        end
+      end
+
+      # Allows to specify an order attribute:
+      #
+      #   User.order('name')
+      #   => SELECT "users".* FROM "users" ORDER BY name
+      #
+      #   User.order('name DESC')
+      #   => SELECT "users".* FROM "users" ORDER BY name DESC
+      #
+      #   User.order('name DESC, email')
+      #   => SELECT "users".* FROM "users" ORDER BY name DESC, email
+      def order(*args)
+        @options[:order] << args
+      end
+
+      # Replaces any existing order defined on the relation with the specified order.
+      #
+      #   User.order('email DESC').reorder('id ASC') # generated SQL has 'ORDER BY id ASC'
+      #
+      # Subsequent calls to order on the same relation will be appended. For example:
+      #
+      #   User.order('email DESC').reorder('id ASC').order('name ASC')
+      #
+      # generates a query with 'ORDER BY name ASC, id ASC'.
+      def reorder(*args)
+        @options[:order] = args
+      end
+
+      def all
+        @options[:conditions] = nil
+        self
+      end
+
+      # Works in two unique ways.
+      #
+      # First: takes a block so it can be used just like Array#select.
+      #
+      #   Model.all.select { |m| m.field == value }
+      #
+      # This will build an array of objects from the database for the scope,
+      # converting them into an array and iterating through them using Array#select.
+      #
+      # Second: Modifies the SELECT statement for the query so that only certain
+      # fields are retrieved:
+      #
+      #   Model.select(:field)
+      #   # => [#<Model field:value>]
+      #
+      # Although in the above example it looks as though this method returns an
+      # array, it actually returns a relation object and can have other query
+      # methods appended to it, such as the other methods in ActiveRecord::QueryMethods.
+      #
+      # The argument to the method can also be an array of fields.
+      #
+      #   Model.select(:field, :other_field, :and_one_more)
+      #   # => [#<Model field: "value", other_field: "value", and_one_more: "value">]
+      #
+      def select(*fields)
+        if block_given?
+          to_a.select { |*block_args| yield(*block_args) }
+        else
+          raise ArgumentError, 'Call this with at least one field' if fields.empty?
+          clone.select!(*fields)
+        end
+      end
+
+      # Like #select, but modifies relation in place.
+      def select!(*fields)
+        @options[:select] ||= fields.join ','
+        self
+      end
+      alias_method :pluck, :select!
+
+      def reverse_order
+        @options[:reversed] = true
+        self
+      end
+
+      def readonly
+        @options[:readonly] = true
+        self
+      end
+
+      def to_a
+        load
+        @records
+      end
+
+      def as_json(options = nil) #:nodoc:
+        to_a.as_json(options)
+      end
+
+      # Returns size of the results (not size of the stored collection)
+      def size
+        loaded? ? @records.length : count
+      end
+
+      # true if there are no records
+      def empty?
+        return @records.empty? if loaded?
+
+        c = count
+        c.respond_to?(:zero?) ? c.zero? : c.empty?
+      end
+
+      # true if there are any records
+      def any?
+        if block_given?
+          to_a.any? { |*block_args| yield(*block_args) }
+        else
+          !empty?
+        end
+      end
+
+      # true if there is more than one record
+      def many?
+        if block_given?
+          to_a.many? { |*block_args| yield(*block_args) }
+        else
+          limit_value ? to_a.many? : size > 1
+        end
+      end
+
+      # find_all_by_
+      # find_by_
+      # find_first_by_
+      # find_last_by_
+      def method_missing(method_name, *args)
+
+        method = method_name.to_s
+
+        if method.end_with? '!'
+          method.chop!
+          error_on_empty = true
+        end
+
+        if method.start_with? 'find_all_by_'
+          attribs = method.gsub /^find_all_by_/, ''
+        elsif method.start_with? 'find_by_'
+          attribs = method.gsub /^find_by_/, ''
+          limit(1)
+        elsif method.start_with? 'find_first_by_'
+          limit(1)
+          find_first = true
+          attribs = method.gsub /^find_first_by_/, ''
+        elsif method.start_with? 'find_last_by_'
+          limit(1)
+          find_last = true
+          attribs = method.gsub /^find_last_by_/, ''
+        else
+          super
+        end
+
+        attribs = attribs.split '_and_'
+        conditions = {}
+        attribs.each { |attr| conditions[attr] = args.shift }
+
+        where(conditions, *args)
+        load
+        raise RecordNotFound if error_on_empty && @records.empty?
+        return @records.first if limit_value == 1
+        @records
+      end
+
+      # Tries to create a new record with the same scoped attributes
+      # defined in the relation. Returns the initialized object if validation fails.
+      #
+      # Expects arguments in the same format as +Base.create+.
+      #
+      # ==== Examples
+      #   users = User.where(name: 'Oscar')
+      #   users.create # #<User id: 3, name: "oscar", ...>
+      #
+      #   users.create(name: 'fxn')
+      #   users.create # #<User id: 4, name: "fxn", ...>
+      #
+      #   users.create { |user| user.name = 'tenderlove' }
+      #   # #<User id: 5, name: "tenderlove", ...>
+      #
+      #   users.create(name: nil) # validation on name
+      #   # #<User id: nil, name: nil, ...>
+      def create(*args, &block)
+        @model_class.create(*args, &block)
+      end
+
+      # Similar to #create, but calls +create!+ on the base class. Raises
+      # an exception if a validation error occurs.
+      #
+      # Expects arguments in the same format as <tt>Base.create!</tt>.
+      def create!(*args, &block)
+        @model_class.create!(*args, &block)
+      end
+
+      # Tries to load the first record; if it fails, then <tt>create</tt> is called with the same arguments as this method.
+      #
+      # Expects arguments in the same format as +Base.create+.
+      #
+      # ==== Examples
+      #   # Find the first user named Pen�lope or create a new one.
+      #   User.where(:first_name => 'Pen�lope').first_or_create
+      #   # => <User id: 1, first_name: 'Pen�lope', last_name: nil>
+      #
+      #   # Find the first user named Pen�lope or create a new one.
+      #   # We already have one so the existing record will be returned.
+      #   User.where(:first_name => 'Pen�lope').first_or_create
+      #   # => <User id: 1, first_name: 'Pen�lope', last_name: nil>
+      #
+      #   # Find the first user named Scarlett or create a new one with a particular last name.
+      #   User.where(:first_name => 'Scarlett').first_or_create(:last_name => 'Johansson')
+      #   # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
+      #
+      #   # Find the first user named Scarlett or create a new one with a different last name.
+      #   # We already have one so the existing record will be returned.
+      #   User.where(:first_name => 'Scarlett').first_or_create do |user|
+      #     user.last_name = "O'Hara"
+      #   end
+      #   # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
+      def first_or_create(attributes={}, &block)
+        result = first
+        unless result
+          attributes = @options[:hash].merge(attributes) if @options[:hash]
+          result = create(attributes, &block)
+        end
+        result
+      end
+
+      # Like <tt>first_or_create</tt> but calls <tt>create!</tt> so an exception is raised if the created record is invalid.
+      #
+      # Expects arguments in the same format as <tt>Base.create!</tt>.
+      def first_or_create!(attributes={}, &block)
+        result = first
+        unless result
+          attributes = @options[:hash].merge(attributes) if @options[:hash]
+          result = create!(attributes, &block)
+        end
+        result
+      end
+
+      # Like <tt>first_or_create</tt> but calls <tt>new</tt> instead of <tt>create</tt>.
+      #
+      # Expects arguments in the same format as <tt>Base.new</tt>.
+      def first_or_initialize(attributes={}, &block)
+        result = first
+        unless result
+          attributes = @options[:hash].merge(attributes) if @options[:hash]
+          result = @model_class.new(attributes, &block)
+        end
+        result
+      end
+
+      # Destroys the records matching +conditions+ by instantiating each
+      # record and calling its +destroy+ method. Each object's callbacks are
+      # executed (including <tt>:dependent</tt> association options and
+      # +before_destroy+/+after_destroy+ Observer methods). Returns the
+      # collection of objects that were destroyed; each will be frozen, to
+      # reflect that no changes should be made (since they can't be
+      # persisted).
+      #
+      # Note: Instantiation, callback execution, and deletion of each
+      # record can be time consuming when you're removing many records at
+      # once. It generates at least one SQL +DELETE+ query per record (or
+      # possibly more, to enforce your callbacks). If you want to delete many
+      # rows quickly, without concern for their associations or callbacks, use
+      # +delete_all+ instead.
+      #
+      # ==== Parameters
+      #
+      # * +conditions+ - A string, array, or hash that specifies which records
+      #   to destroy. If omitted, all records are destroyed. See the
+      #   Conditions section in the introduction to ActiveRecord::Base for
+      #   more information.
+      #
+      # ==== Examples
+      #
+      #   Person.destroy_all("last_login < '2004-04-04'")
+      #   Person.destroy_all(status: "inactive")
+      #   Person.where(:age => 0..18).destroy_all
+      def destroy_all(conditions=nil)
+        if conditions
+          where(conditions).destroy_all
+        else
+          to_a.each {|object| object.destroy}
+          @records
+        end
+      end
+
+      # Destroy an object (or multiple objects) that has the given id. The object is instantiated first,
+      # therefore all callbacks and filters are fired off before the object is deleted. This method is
+      # less efficient than ActiveRecord#delete but allows cleanup methods and other actions to be run.
+      #
+      # This essentially finds the object (or multiple objects) with the given id, creates a new object
+      # from the attributes, and then calls destroy on it.
+      #
+      # ==== Parameters
+      #
+      # * +id+ - Can be either an Integer or an Array of Integers.
+      #
+      # ==== Examples
+      #
+      #   # Destroy a single object
+      #   Foo.destroy(1)
+      #
+      #   # Destroy multiple objects
+      #   foos = [1,2,3]
+      #   Foo.destroy(foos)
+      def destroy(id)
+        if id.is_a?(Array)
+          id.map {|one_id| destroy(one_id)}
+        else
+          find(id).destroy
+        end
+      end
+
+      # Deletes the records matching +conditions+ without instantiating the records
+      # first, and hence not calling the +destroy+ method nor invoking callbacks. This
+      # is a single SQL DELETE statement that goes straight to the database, much more
+      # efficient than +destroy_all+. Be careful with relations though, in particular
+      # <tt>:dependent</tt> rules defined on associations are not honored. Returns the
+      # number of rows affected.
+      #
+      #   Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')")
+      #   Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else'])
+      #   Post.where(:person_id => 5).where(:category => ['Something', 'Else']).delete_all
+      #
+      # Both calls delete the affected posts all at once with a single DELETE statement.
+      # If you need to destroy dependent associations or call your <tt>before_*</tt> or
+      # +after_destroy+ callbacks, use the +destroy_all+ method instead.
+      #
+      # If a limit scope is supplied, +delete_all+ raises an ActiveRecord error:
+      #
+      #   Post.limit(100).delete_all
+      #   # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit scope
+      def delete_all(conditions=nil)
+        raise ActiveRecordError.new("delete_all doesn't support limit scope") if self.limit_value
+
+        if conditions
+          where(conditions).delete_all
+        else
+          pluck :uuid
+          response = load
+          response.each {|entity| entity.delete} # todo: can this be optimized into one call?
+          response.size
+        end
+      end
+
+      # Deletes the row with a primary key matching the +id+ argument, using a
+      # SQL +DELETE+ statement, and returns the number of rows deleted. Active
+      # Record objects are not instantiated, so the object's callbacks are not
+      # executed, including any <tt>:dependent</tt> association options or
+      # Observer methods.
+      #
+      # You can delete multiple rows at once by passing an Array of <tt>id</tt>s.
+      #
+      # Note: Although it is often much faster than the alternative,
+      # <tt>#destroy</tt>, skipping callbacks might bypass business logic in
+      # your application that ensures referential integrity or performs other
+      # essential jobs.
+      #
+      # ==== Examples
+      #
+      #   # Delete a single row
+      #   Foo.delete(1)
+      #
+      #   # Delete multiple rows
+      #   Foo.delete([2,3,4])
+      def delete(id_or_array)
+        if id_or_array.is_a? Array
+          id_or_array.each {|id| @model_class.resource[id].delete} # todo: can this be optimized into one call?
+        else
+          @model_class.resource[id_or_array].delete
+        end
+      end
+
+      # Updates all records with details given if they match a set of conditions supplied, limits and order can
+      # also be supplied. This method sends a single update straight to the database. It does not instantiate
+      # the involved models and it does not trigger Active Record callbacks or validations.
+      #
+      # ==== Parameters
+      #
+      # * +updates+ - hash of attribute updates
+      #
+      # ==== Examples
+      #
+      #   # Update all customers with the given attributes
+      #   Customer.update_all wants_email: true
+      #
+      #   # Update all books with 'Rails' in their title
+      #   Book.where('title LIKE ?', '%Rails%').update_all(author: 'David')
+      #
+      #   # Update all books that match conditions, but limit it to 5 ordered by date
+      #   Book.where('title LIKE ?', '%Rails%').order(:created).limit(5).update_all(:author => 'David')
+      def update_all(updates)
+        raise ArgumentError, "Empty list of attributes to change" if updates.blank?
+        raise ArgumentError, "updates must be a Hash" unless updates.is_a? Hash
+        run_update(updates)
+      end
+
+      # Looping through a collection of records from the database
+      # (using the +all+ method, for example) is very inefficient
+      # since it will try to instantiate all the objects at once.
+      #
+      # In that case, batch processing methods allow you to work
+      # with the records in batches, thereby greatly reducing memory consumption.
+      #
+      # The #find_each method uses #find_in_batches with a batch size of 1000 (or as
+      # specified by the +:batch_size+ option).
+      #
+      #   Person.all.find_each do |person|
+      #     person.do_awesome_stuff
+      #   end
+      #
+      #   Person.where("age > 21").find_each do |person|
+      #     person.party_all_night!
+      #   end
+      #
+      #  You can also pass the +:start+ option to specify
+      #  an offset to control the starting point.
+      def find_each(options = {})
+        find_in_batches(options) do |records|
+          records.each { |record| yield record }
+        end
+      end
+
+      # Yields each batch of records that was found by the find +options+ as
+      # an array. The size of each batch is set by the +:batch_size+
+      # option; the default is 1000.
+      #
+      # You can control the starting point for the batch processing by
+      # supplying the +:start+ option. This is especially useful if you
+      # want multiple workers dealing with the same processing queue. You can
+      # make worker 1 handle all the records between id 0 and 10,000 and
+      # worker 2 handle from 10,000 and beyond (by setting the +:start+
+      # option on that worker).
+      #
+      # It's not possible to set the order. That is automatically set to
+      # ascending on the primary key ("id ASC") to make the batch ordering
+      # work. This also mean that this method only works with integer-based
+      # primary keys. You can't set the limit either, that's used to control
+      # the batch sizes.
+      #
+      #   Person.where("age > 21").find_in_batches do |group|
+      #     sleep(50) # Make sure it doesn't get too crowded in there!
+      #     group.each { |person| person.party_all_night! }
+      #   end
+      #
+      #   # Let's process the next 2000 records
+      #   Person.all.find_in_batches(start: 2000, batch_size: 2000) do |group|
+      #     group.each { |person| person.party_all_night! }
+      #   end
+      def find_in_batches(options={})
+        options.assert_valid_keys(:start, :batch_size)
+
+        raise "Not yet implemented" # todo
+
+        start = options.delete(:start) || 0
+        batch_size = options.delete(:batch_size) || 1000
+
+        while records.any?
+          records_size = records.size
+          primary_key_offset = records.last.id
+
+          yield records
+
+          break if records_size < batch_size
+
+          if primary_key_offset
+            records = relation.where(table[primary_key].gt(primary_key_offset)).to_a
+          else
+            raise "Primary key not included in the custom select clause"
+          end
+        end
+      end
+
+      # Updates an object (or multiple objects) and saves it to the database, if validations pass.
+      # The resulting object is returned whether the object was saved successfully to the database or not.
+      #
+      # ==== Parameters
+      #
+      # * +id+ - This should be the id or an array of ids to be updated.
+      # * +attributes+ - This should be a hash of attributes or an array of hashes.
+      #
+      # ==== Examples
+      #
+      #   # Updates one record
+      #   Person.update(15, user_name: 'Samuel', group: 'expert')
+      #
+      #   # Updates multiple records
+      #   people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } }
+      #   Person.update(people.keys, people.values)
+      def update(id, attributes)
+        if id.is_a?(Array)
+          id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) }
+        else
+          object = find(id)
+          object.update_attributes(attributes)
+          object
+        end
+      end
+
+      ## todo: scoping
+      ## Scope all queries to the current scope.
+      ##
+      ##   Comment.where(:post_id => 1).scoping do
+      ##     Comment.first # SELECT * FROM comments WHERE post_id = 1
+      ##   end
+      ##
+      ## Please check unscoped if you want to remove all previous scopes (including
+      ## the default_scope) during the execution of a block.
+      #def scoping
+      #  previous, @model_class.current_scope = @model_class.current_scope, self
+      #  yield
+      #ensure
+      #  klass.current_scope = previous
+      #end
+
+
+      # #where accepts conditions in one of several formats.
+      #
+      # === string
+      #
+      # A single string, without additional arguments, is used in the where clause of the query.
+      #
+      #    Client.where("orders_count = '2'")
+      #    # SELECT * where orders_count = '2';
+      #
+      # Note that building your own string from user input may expose your application
+      # to injection attacks if not done properly. As an alternative, it is recommended
+      # to use one of the following methods.
+      #
+      # === array
+      #
+      # If an array is passed, then the first element of the array is treated as a template, and
+      # the remaining elements are inserted into the template to generate the condition.
+      # Active Record takes care of building the query to avoid injection attacks, and will
+      # convert from the ruby type to the database type where needed. Elements are inserted
+      # into the string in the order in which they appear.
+      #
+      #   User.where(["name = ? and email = ?", "Joe", "joe@example.com"])
+      #   # SELECT * WHERE name = 'Joe' AND email = 'joe@example.com';
+      #
+      # Alternatively, you can use named placeholders in the template, and pass a hash as the
+      # second element of the array. The names in the template are replaced with the corresponding
+      # values from the hash.
+      #
+      #   User.where(["name = :name and email = :email", { name: "Joe", email: "joe@example.com" }])
+      #   # SELECT * WHERE name = 'Joe' AND email = 'joe@example.com';
+      #
+      # This can make for more readable code in complex queries.
+      #
+      # Lastly, you can use sprintf-style % escapes in the template. This works slightly differently
+      # than the previous methods; you are responsible for ensuring that the values in the template
+      # are properly quoted. The values are passed to the connector for quoting, but the caller
+      # is responsible for ensuring they are enclosed in quotes in the resulting SQL. After quoting,
+      # the values are inserted using the same escapes as the Ruby core method <tt>Kernel::sprintf</tt>.
+      #
+      #   User.where(["name = '%s' and email = '%s'", "Joe", "joe@example.com"])
+      #   # SELECT * WHERE name = 'Joe' AND email = 'joe@example.com';
+      #
+      # If #where is called with multiple arguments, these are treated as if they were passed as
+      # the elements of a single array.
+      #
+      #   User.where("name = :name and email = :email", { name: "Joe", email: "joe@example.com" })
+      #   # SELECT * WHERE name = 'Joe' AND email = 'joe@example.com';
+      #
+      # When using strings to specify conditions, you can use any operator available from
+      # the database. While this provides the most flexibility, you can also unintentionally introduce
+      # dependencies on the underlying database. If your code is intended for general consumption,
+      # test with multiple database backends.
+      #
+      # === hash
+      #
+      # #where will also accept a hash condition, in which the keys are fields and the values
+      # are values to be searched for.
+      #
+      # Fields can be symbols or strings. Values can be single values, arrays, or ranges.
+      #
+      #    User.where({ name: "Joe", email: "joe@example.com" })
+      #    # SELECT * WHERE name = 'Joe' AND email = 'joe@example.com'
+      #
+      #    User.where({ name: ["Alice", "Bob"]})
+      #    # SELECT * WHERE name IN ('Alice', 'Bob')
+      #
+      #    User.where({ created_at: (Time.now.midnight - 1.day)..Time.now.midnight })
+      #    # SELECT * WHERE (created_at BETWEEN '2012-06-09 07:00:00.000000' AND '2012-06-10 07:00:00.000000')
+      #
+      # In the case of a belongs_to relationship, an association key can be used
+      # to specify the model if an ActiveRecord object is used as the value.
+      #
+      #    author = Author.find(1)
+      #
+      #    # The following queries will be equivalent:
+      #    Post.where(:author => author)
+      #    Post.where(:author_id => author)
+      #
+      # === empty condition
+      #
+      # If the condition returns true for blank?, then where is a no-op and returns the current relation.
+      #
+      def where(opts, *rest)
+        return self if opts.blank?
+        case opts
+          when Hash
+            @options[:hash] = opts # keep around for first_or_create stuff...
+            opts.each do |k,v|
+              # todo: can we support IN and BETWEEN syntax as documented above?
+              v = "'#{v}'" if v.is_a? String
+              query_conditions << "#{k} = #{v}"
+            end
+          when String
+            query_conditions << opts
+          when Array
+            query = opts.shift.gsub '?', "'%s'"
+            query = query % opts
+            query_conditions << query
+        end
+        self
+      end
+
+
+      protected
+
+
+      def limit_value
+        @options[:limit]
+      end
+
+      def query_conditions
+        @options[:conditions] ||= []
+      end
+
+      def loaded?
+        !!@records
+      end
+
+      def reversed?
+        !!@options[:reversed]
+      end
+
+      def find_one(id_or_name=nil)
+        begin
+          entity = @model_class.resource[id_or_name].query(nil, limit: 1).entity
+          @model_class.model_name.constantize.new(entity.data) if entity
+        rescue RestClient::ResourceNotFound
+          nil
+        end
+      end
+
+      def find_one!(id_or_name=nil)
+        find_one(id_or_name) or raise RecordNotFound
+      end
+
+      # Server-side options:
+      # Xql	       string   Query in the query language
+      # type	     string   Entity type to return
+      # Xreversed   string   Return results in reverse order
+      # connection string   Connection type (e.g., "likes")
+      # start      string   First entity's UUID to return
+      # cursor     string   Encoded representation of the query position for paging
+      # Xlimit      integer  Number of results to return
+      # permission string   Permission type
+      # Xfilter     string   Condition on which to filter
+      def query_options
+        # todo: support more options?
+        options = {}
+        options.merge!({:limit => limit_value.to_json}) if limit_value
+        options.merge!({:skip => @options[:skip].to_json}) if @options[:skip]
+        options.merge!({:reversed => reversed?.to_json}) if reversed?
+        options.merge!({:order => @options[:order]}) if @options[:order]
+        options.merge!({:cursor => @options[:cursor]}) if @options[:cursor]
+        options
+      end
+
+      def create_query
+        select = @options[:select] || '*'
+        where = ('where ' + query_conditions.join(' and ')) unless query_conditions.blank?
+        "select #{select} #{where}"
+      end
+
+      def run_query
+        @model_class.resource.query(create_query, query_options)
+      end
+
+      def run_update(attributes)
+        @model_class.resource.update_query(attributes, create_query, query_options)
+      end
+
+      def load
+        return if loaded?
+        begin
+          @response = run_query
+          if (!@options[:select] or @options[:select] == '*')
+            @records = @response.entities.collect {|r| @model_class.model_name.constantize.new(r.data)}
+          else # handle list
+            selects = @options[:select].split ','
+            @records = @response.entities.collect do |r|
+              data = {}
+              (0..selects.size).each do |i|
+                data[selects[i]] = r[i]
+              end
+              @model_class.model_name.constantize.new(data)
+            end
+          end
+        rescue RestClient::ResourceNotFound
+          @records = []
+        end
+      end
+    end
+  end
+end

http://git-wip-us.apache.org/repos/asf/usergrid/blob/867060fa/sdks/other/ruby-on-rails/lib/usergrid_ironhorse/user_context.rb
----------------------------------------------------------------------
diff --git a/sdks/other/ruby-on-rails/lib/usergrid_ironhorse/user_context.rb b/sdks/other/ruby-on-rails/lib/usergrid_ironhorse/user_context.rb
new file mode 100644
index 0000000..b7a0804
--- /dev/null
+++ b/sdks/other/ruby-on-rails/lib/usergrid_ironhorse/user_context.rb
@@ -0,0 +1,96 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+module Usergrid
+  module Ironhorse
+    module UserContext
+
+      # returns user if logged in, nil otherwise
+      # if session is passed, stores user_id and auth_token in the session
+      # and sets User as current_user
+      def authenticate(username, password, session=nil)
+        application = Usergrid::Application.new Usergrid::Ironhorse::Base.settings[:application_url]
+        begin
+          application.login username, password
+          user = new application.current_user.data
+          if session
+            session[:usergrid_user_id] = user.id
+            session[:usergrid_auth_token] = user.current_auth_token
+            set_thread_context(session)
+            Thread.current[:usergrid_current_user] = user
+          end
+          user
+        rescue
+          Rails.logger.info $!
+          raise $!
+        end
+      end
+
+      # clears auth from session and thread
+      def clear_authentication(session)
+        session[:usergrid_user_id] = nil
+        session[:usergrid_auth_token] = nil
+        clear_thread_context(session)
+      end
+
+      # allows admin actions to be done in a block
+      def as_admin(&block)
+        save_auth_token = Thread.current[:usergrid_auth_token]
+        begin
+          unless Base.settings[:auth_token]
+            resource = RestClient::Resource.new Base.settings[:application_url]
+            response = resource['token'].post grant_type: 'client_credentials', client_id: Base.settings[:client_id], client_secret: Base.settings[:client_secret]
+            Base.settings[:auth_token] = MultiJson.load(response)['access_token']
+          end
+          Thread.current[:usergrid_auth_token] = Base.settings[:auth_token]
+          yield block
+        ensure
+          Thread.current[:usergrid_auth_token] = save_auth_token
+        end
+      end
+
+      # sets auth for current thread
+      def set_thread_context(session)
+        Thread.current[:usergrid_user_id] = session[:usergrid_user_id]
+        Thread.current[:usergrid_auth_token] = session[:usergrid_auth_token]
+        Thread.current[:usergrid_current_user] = nil
+      end
+      alias_method :set_context, :set_thread_context
+
+      # clears auth from current thread
+      def clear_thread_context
+        Thread.current[:usergrid_user_id] = nil
+        Thread.current[:usergrid_auth_token] = nil
+        Thread.current[:usergrid_current_user] = nil
+      end
+      alias_method :clear_context, :clear_thread_context
+
+      # returns the auth token for the current thread
+      def current_auth_token
+        Thread.current[:usergrid_auth_token]
+      end
+
+      # does a find and return
+      def current_user
+        unless Thread.current[:usergrid_current_user]
+          Thread.current[:usergrid_current_user] = find(Thread.current[:usergrid_user_id]) if Thread.current[:usergrid_user_id]
+        end
+        Thread.current[:usergrid_current_user]
+      end
+
+    end
+  end
+end

http://git-wip-us.apache.org/repos/asf/usergrid/blob/867060fa/sdks/other/ruby-on-rails/lib/usergrid_ironhorse/version.rb
----------------------------------------------------------------------
diff --git a/sdks/other/ruby-on-rails/lib/usergrid_ironhorse/version.rb b/sdks/other/ruby-on-rails/lib/usergrid_ironhorse/version.rb
new file mode 100644
index 0000000..990b730
--- /dev/null
+++ b/sdks/other/ruby-on-rails/lib/usergrid_ironhorse/version.rb
@@ -0,0 +1,21 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+module Usergrid
+  module Ironhorse
+    VERSION = '0.1.2'
+  end
+end

http://git-wip-us.apache.org/repos/asf/usergrid/blob/867060fa/sdks/other/ruby-on-rails/spec/spec_helper.rb
----------------------------------------------------------------------
diff --git a/sdks/other/ruby-on-rails/spec/spec_helper.rb b/sdks/other/ruby-on-rails/spec/spec_helper.rb
new file mode 100644
index 0000000..2cee2c2
--- /dev/null
+++ b/sdks/other/ruby-on-rails/spec/spec_helper.rb
@@ -0,0 +1,93 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+require 'simplecov'
+require 'test/unit'
+require 'rspec'
+require 'yaml'
+require 'securerandom'
+require_relative '../lib/usergrid_ironhorse'
+
+ENV["RAILS_ENV"] ||= 'test'
+Dir[Pathname.new(File.dirname(__FILE__)).join("support/**/*.rb")].each { |f| require f }
+
+RSpec.configure do |config|
+  config.mock_with :rspec
+  config.expect_with :stdlib
+  config.expect_with :rspec
+end
+
+LOG = Logger.new(STDOUT)
+RestClient.log=LOG
+
+SimpleCov.at_exit do
+  SimpleCov.result.format!
+  #index = File.join(SimpleCov.coverage_path, 'index.html')
+  #`open #{index}` if File.exists?(index)
+end
+SimpleCov.start
+
+
+SPEC_SETTINGS = YAML::load_file(File.join File.dirname(__FILE__), 'spec_settings.yaml')
+Usergrid::Ironhorse::Base.configure! nil, nil
+
+def login_management
+  management = Usergrid::Resource.new(SPEC_SETTINGS[:api_url]).management
+  management.login SPEC_SETTINGS[:organization][:username], SPEC_SETTINGS[:organization][:password]
+  management
+end
+
+# ensure we are correctly setup (management login & organization)
+management = login_management
+
+begin
+  management.create_organization(SPEC_SETTINGS[:organization][:name],
+                                 SPEC_SETTINGS[:organization][:username],
+                                 SPEC_SETTINGS[:organization][:username],
+                                 "#{SPEC_SETTINGS[:organization][:username]}@email.com",
+                                 SPEC_SETTINGS[:organization][:password])
+  LOG.info "created organization with user #{SPEC_SETTINGS[:organization][:username]}@email.com"
+rescue
+  if MultiJson.load($!.response)['error'] == "duplicate_unique_property_exists"
+    LOG.debug "test organization exists"
+  else
+    raise $!
+  end
+end
+
+def create_random_application
+  management = login_management
+  organization = management.organization SPEC_SETTINGS[:organization][:name]
+  app_name = "_test_app_#{SecureRandom.hex}"
+  organization.create_application app_name
+  management.application SPEC_SETTINGS[:organization][:name], app_name
+end
+
+def delete_application(application)
+  management = login_management
+  application.auth_token = management.auth_token
+  application.delete rescue nil  # not implemented on server yet
+end
+
+def create_random_user(application, login=false)
+  random = SecureRandom.hex
+  user_hash = {username: "username_#{random}",
+               password: random,
+               email:    "#{random}@email.com",
+               name:     "#{random} name" }
+  entity = application['users'].post(user_hash).entity
+  application.login user_hash[:username], user_hash[:password] if login
+  entity
+end

http://git-wip-us.apache.org/repos/asf/usergrid/blob/867060fa/sdks/other/ruby-on-rails/spec/spec_settings.yaml
----------------------------------------------------------------------
diff --git a/sdks/other/ruby-on-rails/spec/spec_settings.yaml b/sdks/other/ruby-on-rails/spec/spec_settings.yaml
new file mode 100644
index 0000000..815ba43
--- /dev/null
+++ b/sdks/other/ruby-on-rails/spec/spec_settings.yaml
@@ -0,0 +1,20 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+:api_url: http://localhost:8080
+:organization:
+  :name: test-organization
+  :username: test
+  :password: test

http://git-wip-us.apache.org/repos/asf/usergrid/blob/867060fa/sdks/other/ruby-on-rails/spec/support/active_model_lint.rb
----------------------------------------------------------------------
diff --git a/sdks/other/ruby-on-rails/spec/support/active_model_lint.rb b/sdks/other/ruby-on-rails/spec/support/active_model_lint.rb
new file mode 100644
index 0000000..267be05
--- /dev/null
+++ b/sdks/other/ruby-on-rails/spec/support/active_model_lint.rb
@@ -0,0 +1,33 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+# spec/support/active_model_lint.rb
+# adapted from rspec-rails: http://github.com/rspec/rspec-rails/blob/master/spec/rspec/rails/mocks/mock_model_spec.rb
+
+shared_examples_for "ActiveModel" do
+  include ActiveModel::Lint::Tests
+
+  # to_s is to support ruby-1.9
+  ActiveModel::Lint::Tests.public_instance_methods.map{|m| m.to_s}.grep(/^test/).each do |m|
+    example m.sub('_',' ') do
+      send m
+    end
+  end
+
+  def model
+    subject
+  end
+end

http://git-wip-us.apache.org/repos/asf/usergrid/blob/867060fa/sdks/other/ruby-on-rails/spec/usergrid_ironhorse/base_spec.rb
----------------------------------------------------------------------
diff --git a/sdks/other/ruby-on-rails/spec/usergrid_ironhorse/base_spec.rb b/sdks/other/ruby-on-rails/spec/usergrid_ironhorse/base_spec.rb
new file mode 100644
index 0000000..4aa1da6
--- /dev/null
+++ b/sdks/other/ruby-on-rails/spec/usergrid_ironhorse/base_spec.rb
@@ -0,0 +1,452 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+describe Usergrid::Ironhorse::Base do
+
+  it_should_behave_like 'ActiveModel'
+
+  class User < Usergrid::Ironhorse::Base
+    extend Usergrid::Ironhorse::UserContext
+  end
+
+  before :all do
+    @application = create_random_application
+    Foo.configure!(@application.url, @application.auth_token)
+    @user = create_random_user @application, true
+    @foo = (@application.create_entity 'foos', name: 'foo42', answer: 42).entity
+  end
+
+  after :all do
+    @foo.delete
+    @user.delete
+    #delete_application @application # not supported on server yet
+  end
+
+  class Foo < Usergrid::Ironhorse::Base; end
+  Foo.validates :name, :presence => true
+
+  class Bar < Usergrid::Ironhorse::Base; end
+
+  describe 'subclasses should be able to' do
+
+    it "do tasks as admin when requested" do
+      organization = @foo.management.organization SPEC_SETTINGS[:organization][:name]
+      organization.logout
+
+      # should fail under current user's context
+      expect {
+        organization.create_application "_test_app_#{SecureRandom.hex}"
+      }.to raise_error RestClient::Unauthorized
+
+      # should succeed under admin context
+      User.as_admin do
+        organization.create_application "_test_app_#{SecureRandom.hex}"
+      end
+    end
+
+    it "do tasks as admin if require_login is false" do
+      organization = @foo.management.organization SPEC_SETTINGS[:organization][:name]
+      organization.logout
+
+      # should fail under current user's context
+      expect {
+        organization.create_application "_test_app_#{SecureRandom.hex}"
+      }.to raise_error RestClient::Unauthorized
+
+      # should succeed once require_login is false
+      User.settings[:require_login] = false
+      organization.create_application "_test_app_#{SecureRandom.hex}"
+      User.settings[:require_login] = true
+    end
+
+    it 'be created and destroyed' do
+      foo = Foo.create name: 'foo man'
+      foo.persisted?.should be_true
+      foo.name.should eq 'foo man'
+      foo = Foo.find_by_name 'foo man'
+      foo.should_not be_nil
+      foo.destroy.should be_true
+      foo.persisted?.should be_false
+      foo = Foo.find_by_name 'foo man'
+      foo.should be_nil
+    end
+
+    it 'be changed and saved' do
+      foo = Foo.find_by_name @foo.name
+      foo.answer.should eq @foo.answer
+      foo.number = 43
+      foo.changed?.should be_true
+      foo.number.should == 43
+      foo.save!
+      foo = Foo.find_by_name @foo.name
+      foo.number.should == 43
+    end
+
+    it 'be reloaded' do
+      foo = Foo.find @foo.uuid
+      foo.answer = 44
+      foo.changed?.should be_true
+      foo.answer.should == 44
+      foo.reload
+      foo.answer.should == 42
+    end
+
+    it 'be found using find_by_name' do
+      foo = Foo.find_by_name @foo.name
+      foo.uuid.should_not be_nil
+      foo.name.should eq @foo.name
+      foo.should be_a Foo
+      foo.persisted?.should be_true
+    end
+
+    it 'be found using find_by_name!' do
+      foo = Foo.find_by_name! @foo.name
+      foo.uuid.should_not be_nil
+      foo.name.should eq @foo.name
+      foo.should be_a Foo
+      foo.persisted?.should be_true
+    end
+
+    it 'throw a RecordNotFound when find_by_name! misses' do
+      expect { Foo.find_by_name! 'name3' }.to raise_error(ActiveRecord::RecordNotFound)
+    end
+
+    it 'add a validation' do
+      foo = Foo.new
+      foo.valid?.should be_false
+      foo.name = 'joe'
+      foo.valid?.should be_true
+    end
+
+    it 'fail to save an invalid record and save the errors' do
+      foo = Foo.new
+      foo.save.should be_false
+      foo.persisted?.should be_false
+      foo.errors.count.should == 1
+      foo.errors.get(:name).first.should eq "can't be blank"
+    end
+
+    it 'fail to create an invalid record and save the errors' do
+      foo = Foo.create
+      foo.persisted?.should be_false
+      foo.errors.count.should == 1
+      foo.errors.get(:name).first.should eq "can't be blank"
+    end
+
+    it 'fail to save! an invalid record' do
+      expect { Foo.new.save! }.to raise_error(ActiveRecord::RecordNotSaved)
+    end
+
+    it 'fail to create! an invalid record' do
+      expect { Foo.create! }.to raise_error(ActiveRecord::RecordNotSaved)
+    end
+
+    it 'retrieve first' do
+      foo2 = Foo.create! name: 'foo2'
+      foo = Foo.first
+      foo.uuid.should eq @foo.uuid
+      foo2.destroy
+    end
+
+    it 'retrieve last' do
+      foo2 = Foo.create! name: 'foo2'
+      foo = Foo.last
+      foo.uuid.should eq foo2.uuid
+      foo2.destroy
+    end
+
+    it 'take multiple' do
+      foo2 = Foo.create! name: 'foo2'
+      foos = Foo.take(2)
+      foos.size.should == 2
+      foos.first.uuid.should_not be_nil
+      foo2.destroy
+    end
+
+    it 'find multiple by id' do
+      foo2 = Foo.create name: 'foo2'
+      foos = Foo.find @foo.uuid, foo2.uuid
+      foos.size.should == 2
+      foos.first.uuid.should eq @foo.uuid
+      foos.last.uuid.should eq foo2.uuid
+      foo2.destroy
+    end
+
+    it 'find multiple by name' do
+      foo2 = Foo.create! name: 'foo2'
+      foos = Foo.find @foo.name, foo2.name
+      foos.size.should == 2
+      foos.first.uuid.should eq @foo.uuid
+      foos.last.uuid.should eq foo2.uuid
+      foo2.destroy
+    end
+
+    it 'check exists?(string)' do
+      Foo.exists?(@foo.name).should be_true
+    end
+
+    it 'check not exists?(string)' do
+      Foo.exists?('asdfasdf').should be_false
+    end
+
+    it 'check exists?(Array)' do
+      Foo.exists?(['name = ?', @foo.name]).should be_true
+    end
+
+    it 'check not exists?(Array)' do
+      Foo.exists?(['name = ?', 'asdfasdf']).should be_false
+    end
+
+    it 'check exists?(Hash)' do
+      Foo.exists?(name: @foo.name).should be_true
+    end
+
+    it 'check not exists?(Hash)' do
+      Foo.exists?(name: 'asfasdf').should be_false
+    end
+
+    it 'check exists?(nil)' do
+      Foo.exists?.should be_true
+    end
+
+    it 'check not exists?(nil)' do
+      Bar.exists?.should be_false
+    end
+
+    it 'perform first_or_create where found' do
+      foo = Foo.where(name: @foo.name).first_or_create
+      foo.uuid.should eq @foo.uuid
+    end
+
+    it 'perform first_or_create where not found' do
+      foo = Foo.where(name: 'foo2').first_or_create(number: 42)
+      foo.name.should eq 'foo2'
+      foo.number.should == 42
+      foo.destroy
+    end
+
+    it 'perform first_or_create! where found' do
+      foo = Foo.where(name: @foo.name).first_or_create!
+      foo.uuid.should eq @foo.uuid
+    end
+
+    it 'perform first_or_create! where not found' do
+      expect { Foo.where(nuumber: 'foo2').first_or_create! }.to raise_error(ActiveRecord::RecordNotSaved)
+    end
+
+    it 'perform first_or_initialize where found' do
+      foo = Foo.where(name: @foo.name).first_or_initialize(xxx: 42)
+      foo.uuid.should eq @foo.uuid
+      foo.xxx.should be_nil
+    end
+
+    it 'perform first_or_initialize where not found' do
+      foo = Foo.where(name: 'foo2').first_or_initialize(number: 42)
+      foo.name.should eq 'foo2'
+      foo.number.should == 42
+      foo.persisted?.should be_false
+    end
+
+    it "should destroy by id" do
+      foo = Foo.create! name: 'foo'
+      Foo.destroy(foo.id)
+      foo = Foo.find_by_name 'foo'
+      foo.should be_nil
+    end
+
+    it "should destroy by ids" do
+      foo = Foo.create! name: 'foo'
+      foo2 = Foo.create! name: 'foo2'
+      Foo.destroy([foo.id, foo2.id])
+      Foo.find_by_name('foo').should be_nil
+      Foo.find_by_name('foo2').should be_nil
+    end
+
+    it "should destroy_all" do
+      foo = Foo.create! name: 'foo', number: 42
+      foo2 = Foo.create! name: 'foo2', number: 42
+      Foo.destroy_all(number: 42)
+      Foo.find_by_name('foo').should be_nil
+      Foo.find_by_name('foo2').should be_nil
+    end
+
+    it "should delete by id" do
+      foo = Foo.create! name: 'foo'
+      Foo.delete(foo.id)
+      foo = Foo.find_by_name 'foo'
+      foo.should be_nil
+    end
+
+    it "should delete by ids" do
+      foo = Foo.create! name: 'foo'
+      foo2 = Foo.create! name: 'foo2'
+      Foo.delete([foo.id, foo2.id])
+      Foo.find_by_name('foo').should be_nil
+      Foo.find_by_name('foo2').should be_nil
+    end
+
+    it "should delete_all" do
+      foo = Foo.create! name: 'foo', number: 42
+      foo2 = Foo.create! name: 'foo2', number: 42
+      Foo.delete_all(number: 42)
+      Foo.find_by_name('foo').should be_nil
+      Foo.find_by_name('foo2').should be_nil
+    end
+
+    it "should update one" do
+      foo = Foo.create! name: 'foo', number: 42
+      Foo.update foo.uuid, { number: 43 }
+      foo.reload.number.should == 43
+      foo.destroy
+    end
+
+    it "should update multiple" do
+      foo = Foo.create! name: 'foo', number: 42
+      foo2 = Foo.create! name: 'foo2', number: 42
+      updates = { foo.uuid => {number: 43}, foo2.uuid => {number: 44}}
+      Foo.update(updates.keys, updates.values)
+      foo.reload.number.should == 43
+      foo2.reload.number.should == 44
+      foo.destroy
+      foo2.destroy
+    end
+
+    it "should update_all" do
+      foo = Foo.create! name: 'foo', number: 43
+      Foo.where(number: 43).update_all({number: 44})
+      Foo.find_by_number(44).should_not be_nil
+      foo.destroy
+    end
+
+    it "should fail on unaccessible mass assignment" do
+      Foo.attr_accessible :name
+      foo = Foo.create! name: 'foo', number: 43
+      foo.number.should_not eq 43
+      foo.update_attributes number: 44, foo: 'bar'
+      foo.number.should_not eq 44
+      foo.destroy
+      Foo._accessible_attributes = nil
+    end
+
+    it "should fail on protected mass assignment" do
+      Foo.attr_protected :number
+      foo = Foo.create! name: 'foo', number: 43
+      foo.number.should_not eq 43
+      foo.update_attributes number: 44, foo: 'bar'
+      foo.number.should_not eq 44
+      foo.destroy
+      Foo._protected_attributes = nil
+    end
+
+    it "should be able to page through results" do
+      bars = (1..15).collect do |i|
+        Bar.create! name: "name_#{i}", value: "value_#{i+1}"
+      end
+      query = Bar.all
+      page = query.to_a # first page
+      page.count.should eq 10
+      page = query.next_page # second page
+      count = 10
+      page.each do |bar|
+        bars[count].name.should eq bar.name
+        count += 1
+      end
+      count.should eq bars.count
+      bars.each {|bar| bar.delete}
+    end
+
+    it "should iterate past page boundaries" do
+      bars = (1..15).collect do |i|
+        Bar.create! name: "name_#{i}", value: "value_#{i+1}"
+      end
+      count = 0
+      Bar.all.each do |bar|
+        bars[count].name.should eq bar.name
+        count += 1
+      end
+      count.should eq bars.count
+      bars.each {|bar| bar.delete}
+    end
+
+    it "should honor limit" do
+      bars = (1..15).collect do |i|
+        Bar.create! name: "name_#{i}", value: "value_#{i+1}"
+      end
+      count = 0
+      Bar.limit(13).each do |bar|
+        bars[count].name.should eq bar.name
+        count += 1
+      end
+      count.should eq 13
+      bars.each {|bar| bar.delete}
+    end
+
+    it "perform as admin only when requested if require_login is true" do
+
+      organization = @foo.management.organization SPEC_SETTINGS[:organization][:name]
+
+      creds = nil
+      User.as_admin do
+        creds = organization.credentials
+      end
+
+      Usergrid::Ironhorse::Base.settings[:client_id] = creds.data.credentials.client_id
+      Usergrid::Ironhorse::Base.settings[:client_secret] = creds.data.credentials.client_secret
+      Usergrid::Ironhorse::Base.settings[:auth_token] = nil
+      User.clear_thread_context
+      organization.logout
+      User.settings[:require_login] = true
+
+      # should fail (login is required)
+      expect {
+        organization.create_application "_test_app_#{SecureRandom.hex}"
+      }.to raise_error RestClient::Unauthorized
+
+      # should succeed under admin context
+      User.as_admin do
+        organization.create_application "_test_app_#{SecureRandom.hex}"
+      end
+
+      Usergrid::Ironhorse::Base.settings[:client_id] = nil
+      Usergrid::Ironhorse::Base.settings[:client_secret] = nil
+    end
+
+    it "perform as admin if require_login is false" do
+
+      organization = @foo.management.organization SPEC_SETTINGS[:organization][:name]
+      organization.logout
+
+      creds = nil
+      User.as_admin do
+        creds = organization.credentials
+      end
+
+      Usergrid::Ironhorse::Base.settings[:client_id] = creds.data.credentials.client_id
+      Usergrid::Ironhorse::Base.settings[:client_secret] = creds.data.credentials.client_secret
+      Usergrid::Ironhorse::Base.settings[:auth_token] = nil
+      User.clear_thread_context
+      organization.logout
+
+      User.settings[:require_login] = false
+
+      # should succeed
+      organization.create_application "_test_app_#{SecureRandom.hex}"
+
+      Usergrid::Ironhorse::Base.settings[:client_id] = nil
+      Usergrid::Ironhorse::Base.settings[:client_secret] = nil
+    end
+  end
+end

http://git-wip-us.apache.org/repos/asf/usergrid/blob/867060fa/sdks/other/ruby-on-rails/usergrid_ironhorse.gemspec
----------------------------------------------------------------------
diff --git a/sdks/other/ruby-on-rails/usergrid_ironhorse.gemspec b/sdks/other/ruby-on-rails/usergrid_ironhorse.gemspec
new file mode 100644
index 0000000..430c5c1
--- /dev/null
+++ b/sdks/other/ruby-on-rails/usergrid_ironhorse.gemspec
@@ -0,0 +1,44 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# -*- encoding: utf-8 -*-
+lib = File.expand_path('../lib', __FILE__)
+$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
+require 'usergrid_ironhorse/version'
+
+Gem::Specification.new do |gem|
+  gem.name          = "usergrid_ironhorse"
+  gem.version       = Usergrid::Ironhorse::VERSION
+  gem.authors       = ["Scott Ganyo"]
+  gem.email         = ["scott@ganyo.com"]
+  gem.description   = %q{Rails ActiveModel gem to access Usergrid / Apigee App Services}
+  gem.summary       = %q{Usergrid_ironhorse enables simple ActiveModel access to Apigee's App Services
+                        (aka Usergrid) REST API for Rails developers.}
+  gem.homepage      = "https://github.com/scottganyo/usergrid_ironhorse"
+
+  gem.files         = `git ls-files`.split($/)
+  gem.executables   = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
+  gem.test_files    = gem.files.grep(%r{^(test|spec|features)/})
+  gem.require_paths = ["lib"]
+
+  gem.add_dependency 'usergrid_iron', '0.9.1'
+  gem.add_dependency 'activemodel', '~> 3.2'
+  gem.add_dependency 'activerecord', '~> 3.2'
+  gem.add_dependency 'i18n'
+
+  gem.add_development_dependency 'rake'
+  gem.add_development_dependency 'rspec'
+  gem.add_development_dependency 'simplecov'
+end

http://git-wip-us.apache.org/repos/asf/usergrid/blob/867060fa/sdks/other/ruby/.gitignore
----------------------------------------------------------------------
diff --git a/sdks/other/ruby/.gitignore b/sdks/other/ruby/.gitignore
new file mode 100644
index 0000000..30b994d
--- /dev/null
+++ b/sdks/other/ruby/.gitignore
@@ -0,0 +1,18 @@
+*.gem
+*.rbc
+.bundle
+.config
+.yardoc
+Gemfile.lock
+InstalledFiles
+_yardoc
+coverage
+doc/
+lib/bundler/man
+pkg
+rdoc
+spec/reports
+test/tmp
+test/version_tmp
+tmp
+.idea
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/usergrid/blob/867060fa/sdks/other/ruby/.rspec
----------------------------------------------------------------------
diff --git a/sdks/other/ruby/.rspec b/sdks/other/ruby/.rspec
new file mode 100644
index 0000000..c99d2e7
--- /dev/null
+++ b/sdks/other/ruby/.rspec
@@ -0,0 +1 @@
+--require spec_helper

http://git-wip-us.apache.org/repos/asf/usergrid/blob/867060fa/sdks/other/ruby/.rvmrc
----------------------------------------------------------------------
diff --git a/sdks/other/ruby/.rvmrc b/sdks/other/ruby/.rvmrc
new file mode 100644
index 0000000..55148ad
--- /dev/null
+++ b/sdks/other/ruby/.rvmrc
@@ -0,0 +1,2 @@
+rvm_gemset_create_on_use_flag=1
+rvm gemset use 'usergrid_iron'

http://git-wip-us.apache.org/repos/asf/usergrid/blob/867060fa/sdks/other/ruby/Gemfile
----------------------------------------------------------------------
diff --git a/sdks/other/ruby/Gemfile b/sdks/other/ruby/Gemfile
new file mode 100644
index 0000000..9a6d84d
--- /dev/null
+++ b/sdks/other/ruby/Gemfile
@@ -0,0 +1,19 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+source 'https://rubygems.org'
+
+# Specify your gem's dependencies in usergrid_iron.gemspec
+gemspec

http://git-wip-us.apache.org/repos/asf/usergrid/blob/867060fa/sdks/other/ruby/LICENSE
----------------------------------------------------------------------
diff --git a/sdks/other/ruby/LICENSE b/sdks/other/ruby/LICENSE
new file mode 100644
index 0000000..ae1e83e
--- /dev/null
+++ b/sdks/other/ruby/LICENSE
@@ -0,0 +1,14 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.

http://git-wip-us.apache.org/repos/asf/usergrid/blob/867060fa/sdks/other/ruby/README.md
----------------------------------------------------------------------
diff --git a/sdks/other/ruby/README.md b/sdks/other/ruby/README.md
new file mode 100644
index 0000000..2f6c594
--- /dev/null
+++ b/sdks/other/ruby/README.md
@@ -0,0 +1,268 @@
+# Ruby SDK
+
+Usergrid_iron enables simple, low-level Ruby access to Apigee's App Services (aka Usergrid)
+REST API with minimal dependencies.
+
+## Installation
+
+### Project
+Add the gem to your project's Gemfile:
+
+    gem 'usergrid_iron'
+
+Then rebuild your bundle:
+
+    $ bundle
+
+### Stand-alone script
+Just manually install the gem:
+
+    $ gem install usergrid_iron
+
+
+## Usage
+
+### Prerequisite
+You'll want to be at least a little bit familiar with Usergrid / Apigee's App Services before you jump in here - but it easy and it's great! Start here:
+
+  App Services docs: <http://apigee.com/docs/usergrid/>
+  Open source stack: <https://github.com/apigee/usergrid-stack>
+
+Awesome. Let's go!
+
+### Getting started with the Usergrid_iron SDK is simple!
+
+#### Let's start with the basics.
+For this example, we'll assume you've already set up an organization, application, and user -
+just fill in your own values in the code below.
+
+```ruby
+require 'usergrid_iron'
+
+# fill in your values here!
+usergrid_api = 'http://localhost:8080'
+organization = ''
+application = ''
+username = ''
+password = ''
+
+# create the base application resource
+# (this is a RestClient.resource)
+application = Usergrid::Application.new "#{usergrid_api}/#{organization}/#{application}"
+
+# login (note: not required for sandbox)
+application.login username, password
+
+# create and store a new dog on the server
+# (the "response" is an enhanced RestClient.response)
+response = application.create_dog breed: 'Black Mouth Cur', name: 'Old Yeller'
+
+# the response has returned the entity data
+# response.entity wraps it in an easy-to-use object
+dog = response.entity
+
+# it's persistent now, so it has a unique id
+uuid = dog.uuid
+
+# we can retrieve the dog by UUID using Hash syntax and calling get()
+# (all dogs are stored in the "dogs" collection)
+response = application["dogs"][uuid].get
+same_dog = response.entity
+
+# we could also retrieve the dog by name
+# we could also use path ('/') syntax instead of nested Hash
+# and we can even skip get() - entity() will do it for us
+same_dog = application["dogs/#{dog.name}"].entity
+
+# is it our dog? well, he knows his name!
+# (and we can retrieve its values by dot or Hash)
+puts "My dog's name is: #{same_dog.name} and his breed is #{same_dog['breed']}"
+```
+
+Well that was really easy. More comments than code! :)
+
+#### Let's try something slightly more complex.
+Let's say you've registered for an organization, but you don't have an application yet
+(or want to create a new one to work on). No worries, just fill in your organization and
+superuser credentials below, and follow along!
+(Better yet: If you used the Usergrid launcher and let it initialize your database,
+you shouldn't need to do anything!)
+
+```ruby
+  require 'usergrid_iron'
+
+  usergrid_api = 'http://localhost:8080'
+  org_name = 'test-organization'
+  username = 'test'
+  password = 'test'
+  app_name = 'dog_sitter'
+
+  ## first, let's get that setup out of the way... ##
+
+  # get a management context & login the superuser
+  management = Usergrid::Management.new usergrid_api
+  management.login username, password
+
+  # get the organization context & create a new application
+  organization = management.organization org_name
+  new_application = organization.create_application app_name
+
+  # create an user for our application
+  new_application.create_user username: 'username', password: 'password'
+
+  # login to our new application as our new user
+  application = organization.application app_name
+  application.login 'username', 'password'
+
+
+  ## now we can play with the puppies! ##
+
+  # we can start with our dog again
+  application.create_dog breed: 'Black Mouth Cur', name: 'Old Yeller'
+
+  # but this time let's create several more dogs all at once
+  application.create_dogs [
+      { breed: 'Catalan sheepdog', name: 'Einstein' },
+      { breed: 'Cocker Spaniel', name: 'Lady' },
+      { breed: 'Mixed', name: 'Benji' }]
+
+  # retrieve all the dogs (well, the first 'page' anyway) and tell them hi!
+  # note: we're calling collection() instead of entity() because we have several
+  dogs = application['dogs'].collection
+
+  # you can iterate a collection just like an array
+  dogs.each do |dog|
+    puts "Hello, #{dog.name}!"
+  end
+
+  # Let's get Benji ("Benji, come!"), but this time we'll retrieve by query
+  response = dogs.query "select * where name = 'Benji'"
+
+  # we could call "response.collection.first"
+  # but there's a shortcut: entity() will also return the first
+  benji = response.entity
+
+  # modify Benji's attributes & save to the server
+  benji.location = 'home'                     # use attribute access
+  benji['breed'] = 'American Cocker Spaniel'  # or access attributes like a Hash
+  benji.save
+
+  # now query for the dogs that are home (should just be Benji)
+  dogs = application['dogs'].query("select * where location = 'home'").collection
+  if dogs.size == 1 && dogs.first.location == 'home'
+    puts "Benji's home!"
+  end
+
+```
+
+Whew. That's enough for now. But looking for a specific feature? Check out the [rspecs](http://github.com/scottganyo/usergrid_iron/tree/master/spec/usergrid/core),
+there are examples of nearly everything!
+
+
+## Contributing
+
+We welcome your enhancements!
+
+1. Fork it
+2. Create your feature branch (`git checkout -b my-new-feature`)
+3. Write some broken rspecs.
+4. Fix the rspecs with your new code.
+3. Commit your changes (`git commit -am 'Added some feature'`)
+4. Push your changes to the upstream branch (`git push origin my-new-feature`)
+5. Create new Pull Request
+
+We've got 100% rspec coverage and we're looking to keep it that way!
+In order to run the tests, check out the Usergrid open source project
+(https://github.com/apigee/usergrid-stack), build, and launch it locally.
+
+(Note: If you change your local Usergrid setting from the default, be sure to update
+usergrid_iron/spec/spec_settings.yaml to match.)
+
+
+## Release notes
+
+### 0.9.2
+* New features
+  1. delete_query (batch delete by query)
+
+### 0.9.1
+* New features
+  1. may now login using credentials: application.login_credentials() or organization.login_credentials()
+
+### 0.9.0
+* Backend changes
+  1. login function now uses POST instead of GET
+
+### 0.0.9
+* Backend changes
+  1. made Resource::response accessor public to support ugc
+
+### 0.0.8
+* Bug fixes
+  1. better handling of paging
+
+### 0.0.7
+* Bug fixes
+  1. multiple_entities? should check data['list']
+
+### 0.0.6
+* New features
+  1. iterators can now optionally cross page boundaries, eg. `collection.follow_cursor.each`
+  2. added facebook_login(fb_access_token) method to application
+
+### 0.0.5
+* New features
+  1. added create_* method for application
+* Backend changes
+  1. eliminated serialization of reserved attributes
+* Incompatible changes
+  1. deprecated `Application::create_user username, password, ...`, use `create_user username: 'user', password: 'password'`
+
+### 0.0.4
+* New features
+  1. empty? check for collection
+  2. update queries (batch update)
+* Backend changes
+  1. Additional data sanity checks
+  2. Additional flexibility in concat_urls
+  3. Cleanup
+
+### 0.0.3
+* New features
+  1. Support for lists returned when making parameterized queries:
+	 <pre>select username, email where\u2026</pre>
+	 or replacement queries:
+	 <pre>select { user:username, email:email } where\u2026
+* Incompatible changes
+  1. Application.create_user parameter change from:
+     <pre>create_user(username, name, email, password, invite=false)</pre>
+     to:
+     <pre>create_user(username, password, email=nil, name=nil, invite=false)</pre>
+* Backend changes
+  1. Replaced json_pure dependency with multi_json
+
+
+## Notes
+
+The following features are not currently implemented on the server:
+
+* delete organization
+* delete application
+* delete user
+
+
+## License
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+the ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.

http://git-wip-us.apache.org/repos/asf/usergrid/blob/867060fa/sdks/other/ruby/Rakefile
----------------------------------------------------------------------
diff --git a/sdks/other/ruby/Rakefile b/sdks/other/ruby/Rakefile
new file mode 100644
index 0000000..bc86b58
--- /dev/null
+++ b/sdks/other/ruby/Rakefile
@@ -0,0 +1,22 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#!/usr/bin/env rake
+require "bundler/gem_tasks"
+
+require 'rspec/core/rake_task'
+RSpec::Core::RakeTask.new(:spec)
+
+RSpec::Core::RakeTask.new("spec:coverage")

http://git-wip-us.apache.org/repos/asf/usergrid/blob/867060fa/sdks/other/ruby/bin/autospec
----------------------------------------------------------------------
diff --git a/sdks/other/ruby/bin/autospec b/sdks/other/ruby/bin/autospec
new file mode 100755
index 0000000..64dcb9c
--- /dev/null
+++ b/sdks/other/ruby/bin/autospec
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'autospec' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+  Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('rspec-core', 'autospec')

http://git-wip-us.apache.org/repos/asf/usergrid/blob/867060fa/sdks/other/ruby/bin/htmldiff
----------------------------------------------------------------------
diff --git a/sdks/other/ruby/bin/htmldiff b/sdks/other/ruby/bin/htmldiff
new file mode 100755
index 0000000..c70e238
--- /dev/null
+++ b/sdks/other/ruby/bin/htmldiff
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'htmldiff' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+  Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('diff-lcs', 'htmldiff')

http://git-wip-us.apache.org/repos/asf/usergrid/blob/867060fa/sdks/other/ruby/bin/ldiff
----------------------------------------------------------------------
diff --git a/sdks/other/ruby/bin/ldiff b/sdks/other/ruby/bin/ldiff
new file mode 100755
index 0000000..8e3524a
--- /dev/null
+++ b/sdks/other/ruby/bin/ldiff
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'ldiff' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+  Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('diff-lcs', 'ldiff')

http://git-wip-us.apache.org/repos/asf/usergrid/blob/867060fa/sdks/other/ruby/bin/restclient
----------------------------------------------------------------------
diff --git a/sdks/other/ruby/bin/restclient b/sdks/other/ruby/bin/restclient
new file mode 100755
index 0000000..4d7bdcf
--- /dev/null
+++ b/sdks/other/ruby/bin/restclient
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'restclient' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+  Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('rest-client', 'restclient')

http://git-wip-us.apache.org/repos/asf/usergrid/blob/867060fa/sdks/other/ruby/bin/rspec
----------------------------------------------------------------------
diff --git a/sdks/other/ruby/bin/rspec b/sdks/other/ruby/bin/rspec
new file mode 100755
index 0000000..0c86b5c
--- /dev/null
+++ b/sdks/other/ruby/bin/rspec
@@ -0,0 +1,16 @@
+#!/usr/bin/env ruby
+#
+# This file was generated by Bundler.
+#
+# The application 'rspec' is installed as part of a gem, and
+# this file is here to facilitate running it.
+#
+
+require 'pathname'
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
+  Pathname.new(__FILE__).realpath)
+
+require 'rubygems'
+require 'bundler/setup'
+
+load Gem.bin_path('rspec-core', 'rspec')

http://git-wip-us.apache.org/repos/asf/usergrid/blob/867060fa/sdks/other/ruby/lib/usergrid/core/application.rb
----------------------------------------------------------------------
diff --git a/sdks/other/ruby/lib/usergrid/core/application.rb b/sdks/other/ruby/lib/usergrid/core/application.rb
new file mode 100644
index 0000000..9cc85c4
--- /dev/null
+++ b/sdks/other/ruby/lib/usergrid/core/application.rb
@@ -0,0 +1,84 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+module Usergrid
+  class Application < Resource
+
+    def initialize(url, options={})
+      org_name = url.split('/')[-2]
+      api_url = url[0..url.index(org_name)-2]
+      super url, api_url, options
+    end
+
+    # note: collection_name s/b plural, but the server will change it if not
+    def create_entity(collection_name, entity_data)
+      self[collection_name].post entity_data
+    end
+    alias_method :create_entities, :create_entity
+
+    # allow create_something(hash_or_array) method
+    def method_missing(method, *args, &block)
+      method_s = method.to_s
+      if method_s.start_with? 'create_'
+        entity = method_s.split('_')[1]
+        return _create_user *args if entity == 'user' && args[0].is_a?(String) # backwards compatibility
+        create_entity entity, *args
+      elsif method_s.end_with? 's' # shortcut for retrieving collections
+        self[method].query(*args)
+      else
+        super method, args, block
+      end
+    end
+
+    def counter_names
+      self['counters'].get.data.data
+    end
+
+    # other_params: 'start_time' (ms), 'end_time' (ms), 'resolution' (minutes)
+    def counter(name, other_params={})
+      options = other_params.merge({counter: name})
+      self['counters'].get({params: options})
+    end
+
+    # login with Facebook token. matching user will be created in usergrid as needed.
+    # usergrid auth token automatically set in auth header for future requests
+    def facebook_login(access_token)
+      params = { fb_access_token: access_token }
+      response = self['auth/facebook'].get({ params: params })
+      self.auth_token = response.data['access_token']
+      user_uuid = response.data['user']['uuid']
+      @current_user = self["/users/#{user_uuid}"].get.entity
+      response
+    end
+
+    def login_credentials(client_id, client_secret)
+      response = self['token'].post grant_type: 'client_credentials', client_id: client_id, client_secret: client_secret
+      self.auth_token = response.data['access_token']
+    end
+
+    private
+
+    def _create_user(username, password, email=nil, name=nil, invite=false)
+      LOG.warn "create_user(username, password, ...) is deprecated"
+      user_hash = { username: username,
+                    password: password,
+                    email: email,
+                    name: name,
+                    invite: invite }
+      create_entity 'users', user_hash
+    end
+
+  end
+end