Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(WIP) Added support for defining named scopes #15

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,28 @@ task = Task.new
task.save # will save, true if successful, false if failed
task.save! # will throw MotionDataWrapper::RecordNotSaved if failed, contains errors object for validation messages
```

### Scopes
Scopes allow you to store named queries as class methods on your model as well as an instance method on the returned Relation for chainability. You can combine scopes to construct complex queries quickly, and keep your query logic DRY.

```ruby
class Task
scope :overdue, ->{ where "date < ?", NSDate.date }
end

Task.overdue
# => Relation

Task.overdue.count
# => 0

Task.where("title = ?", "My Task").overdue
# => Relation

Task.overdue.exists?
# => false
```

### Callbacks
MotionDataWrapper adds support for callbacks in the object lifecycle. Note that unlike ActiveRecord in Rails, these are not class methods that accept symbols or procs, but rather an instance method that the framework will call if defined. None of the methods take arguments, and the return values do not alter the lifecycle in any way (open a PR if you want to add that!)

Expand Down
1 change: 1 addition & 0 deletions lib/motion_data_wrapper/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class Model < NSManagedObject
include FinderMethods
include Persistence
include Validations
include Scoping

def inspect
properties = []
Expand Down
39 changes: 39 additions & 0 deletions lib/motion_data_wrapper/model/scoping.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
module MotionDataWrapper
class Model < NSManagedObject
module Scoping
def self.included(base)
base.extend(ClassMethods)
end

module ClassMethods

# Scopes allow you to store named queries as class methods on your model
# as well as an instance method on the returned Relation for chainability
#
# Examples:
# class Task
# scope :overdue, ->{ where "date < ?", NSDate.date }
# end
#
# Task.overdue => Relation
# Task.overdue.count
#
# Task.where("title = ?", "My Task").overdue
# Task.overdue.exists?
#
def scope(name, proc)
raise ArgumentError, "'#{name}' must be a Symbol" unless name.is_a?(Symbol)
raise ArgumentError, "'#{proc}' must be a Proc" unless proc.is_a?(Proc)

raise ArgumentError, "cannot redefine class method '#{name}'" if respond_to?(name)
raise ArgumentError, "cannot redefine '#{name}' method on Relation" if Relation.instance_methods.include? name

define_singleton_method name, &proc
Relation.send :define_method, name, &proc
end

private :scope
end
end
end
end
63 changes: 63 additions & 0 deletions spec/scoping_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
describe 'MotionDataWrapper::Model scoping' do

after do
clean_core_data
end

describe 'starting from class' do
it "#scope should define a class method" do
Task.should.respond_to :mine
end

it "#scope should expect a symbol as the first argument" do
should.raise(ArgumentError) { Task.send :scope, 'string', ->{} }
end

it "#scope should expect a proc as the second argument" do
should.raise(ArgumentError) { Task.send :scope, :symbol, nil }
end

it "class method should return a relation" do
Task.mine.class.should.be == MotionDataWrapper::Relation
end

it "should be able to define scope with arguments" do
Task.create! title: "My Task"
Task.should.respond_to :with_title

relation = Task.with_title("task")
relation.count.should.be == 1
end
end

describe 'scoping a relation' do
before do
Task.create! due: NSDate.dateWithTimeIntervalSinceNow(-86400), title: "Yesterday Task"
Task.create! due: NSDate.dateWithTimeIntervalSinceNow(86400), title: "Tommorrow Task"
end

it "should allow chaining" do
relation = Task.with_title("task")

relation.count.should.be == 2
relation.overdue.count.should.be == 1
end
end

describe 'redefing existing method' do
it "should raise error if redefining existing class method" do
should.raise(ArgumentError) { Task.send :scope, :create, ->{} }
end

it "should raise error if redefining existing instance method on Relation" do
# Task.first! does not exist, but does exist on relation
should.raise(ArgumentError) { Task.send :scope, :first!, ->{} }
end
end
end

class Task
scope :mine, ->{ where "title = ?", "My Task" }
scope :with_title, ->(title) { where "title contains[cd] ?", title }
scope :overdue, ->{ where "due < ?", NSDate.date }
end