placid 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ Gemfile.lock
2
+ .yardoc/*
3
+ .rvmrc
4
+ pkg/*
5
+ coverage/*
6
+ doc/*
7
+ *~
data/.yardopts ADDED
@@ -0,0 +1 @@
1
+ --markup markdown
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
data/MIT-LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2012 Society for Human Resource Management
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
data/README.md ADDED
@@ -0,0 +1,87 @@
1
+ placid
2
+ ======
3
+
4
+ Placid is an ActiveRecord-ish model using a REST API for storage. The REST API
5
+ can be any backend you choose or create yourself, provided it follows some basic
6
+ conventions.
7
+
8
+ Installation
9
+ ------------
10
+
11
+ $ gem install placid
12
+
13
+ Usage
14
+ -----
15
+
16
+ Define a subclass with the name of your REST model:
17
+
18
+ class Person < Placid::Model
19
+ end
20
+
21
+ and you'll get these class methods, and their REST equivalents:
22
+
23
+ Person.list # GET /people
24
+ Person.create(attrs) # POST /person (attrs)
25
+ Person.find(id) # GET /person/:id
26
+ Person.destroy(id) # DELETE /person/:id
27
+ Person.update(id, attrs) # PUT /person/:id (attrs)
28
+
29
+ By default, placid assumes that your REST API is running on `localhost`. To
30
+ change this, set:
31
+
32
+ Placid::Config.rest_url = 'http://my.rest.host:8080'
33
+
34
+ Each model has a field that is used for uniquely identifying instances. This
35
+ would be called the "primary key" in a relational database. If you don't
36
+ specify the name of the field, `id` is assumed. If your model uses a
37
+ different field name, you can specify it like this:
38
+
39
+ class Person < Placid::Model
40
+ unique_id :email
41
+ end
42
+
43
+ The `Placid::Model` base class includes helper methods for basic HTTP requests.
44
+ You can use these from any model instance, or call them from custom methods you
45
+ define on your model. For example:
46
+
47
+ class Person < Placid::Model
48
+ unique_id :email
49
+
50
+ def add_phone(phone_number)
51
+ put(model, id, 'add_phone', phone_number)
52
+ end
53
+ end
54
+
55
+ jenny = Person.new(:email => 'jenny@example.com')
56
+
57
+ jenny.add_phone('867-5309')
58
+ # Same as:
59
+ jenny.put('person', 'jenny@example.com', 'add_phone', '867-5309')
60
+
61
+
62
+ License
63
+ -------
64
+
65
+ The MIT License
66
+
67
+ Copyright (c) 2012 Society for Human Resource Management
68
+
69
+ Permission is hereby granted, free of charge, to any person obtaining
70
+ a copy of this software and associated documentation files (the
71
+ "Software"), to deal in the Software without restriction, including
72
+ without limitation the rights to use, copy, modify, merge, publish,
73
+ distribute, sublicense, and/or sell copies of the Software, and to
74
+ permit persons to whom the Software is furnished to do so, subject to
75
+ the following conditions:
76
+
77
+ The above copyright notice and this permission notice shall be
78
+ included in all copies or substantial portions of the Software.
79
+
80
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
81
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
82
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
83
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
84
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
85
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
86
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
87
+
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ require 'rake'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec) do |t|
5
+ t.pattern = 'spec/**/*.rb'
6
+ t.rspec_opts = ['--color', '--format doc']
7
+ t.rcov = true
8
+ t.rcov_opts = [
9
+ '--exclude /.gem/,/gems/,spec',
10
+ ]
11
+ end
12
+
@@ -0,0 +1,14 @@
1
+ module Placid
2
+ class Config
3
+ class << self
4
+ attr_accessor :rest_url
5
+
6
+ def default_url
7
+ 'http://localhost'
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ Placid::Config.rest_url = Placid::Config.default_url
14
+
@@ -0,0 +1,4 @@
1
+ module Placid
2
+ class JSONParseError < RuntimeError
3
+ end
4
+ end
@@ -0,0 +1,182 @@
1
+ require 'uri'
2
+ require 'json'
3
+ require 'hashie'
4
+ require 'rest-client'
5
+ require 'placid/exceptions'
6
+
7
+ module Placid
8
+ module Helper
9
+ # Escape any special URI characters in `text` and return the escaped string.
10
+ # `nil` is treated as an empty string.
11
+ #
12
+ # @return [String]
13
+ #
14
+ def escape(text)
15
+ URI.escape(text.to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
16
+ end
17
+
18
+ # If the last arg in `args` is a hash, pop it off and return it. Otherwise,
19
+ # return an empty hash. `args` is modified in-place. Behaves like
20
+ # ActiveSupport's `String#extract_options!` method.
21
+ #
22
+ # @param [Array] args
23
+ # Zero or more arguments, the last of which might be a Hash.
24
+ #
25
+ # @return [Hash]
26
+ #
27
+ def extract_options(args)
28
+ args.last.is_a?(::Hash) ? args.pop : {}
29
+ end
30
+
31
+ # Return a full URL for a REST API request to the given path, relative to
32
+ # the configured `Placid::Config.rest_url`. Each path component is
33
+ # URI-escaped.
34
+ #
35
+ # @example
36
+ # url('people', 'eric') #=> 'http://localhost/people/eric'
37
+ # url('a b', 'c:d') #=> 'http://localhost/a%20b/c%3Ad'
38
+ #
39
+ # @param [Array] path
40
+ # Parts of the path to request. These will be escaped and joined with '/'.
41
+ #
42
+ # @return [String]
43
+ #
44
+ def url(*path)
45
+ url = Placid::Config.rest_url.to_s.gsub(/\/$/, '')
46
+ joined_path = path.map { |p| escape(p) }.join('/')
47
+ return "#{url}/#{joined_path}"
48
+ end
49
+
50
+ # Send a request and return the parsed JSON response.
51
+ #
52
+ # @example
53
+ # request('get', 'people', 'eric')
54
+ # request(:put, 'people', 'eric', {:title => "Developer"})
55
+ #
56
+ # @overload request(method, *path, params={})
57
+ # @param [String, Symbol] method
58
+ # Request method to use ('get', 'post', 'put', 'delete', etc.)
59
+ # @param [Array] path
60
+ # Path components for the request
61
+ # @param [Hash] params
62
+ # Optional parameters to send in the request.
63
+ #
64
+ # @return [Hash]
65
+ # Parsed response, or an empty hash if parsing failed
66
+ #
67
+ def request(method, *path)
68
+ method = method.to_sym
69
+ params = extract_options(path)
70
+ params = {:params => params} if method == :get
71
+ base_url = Placid::Config.rest_url
72
+ begin
73
+ response = RestClient.send(method, url(*path), params)
74
+ rescue RestClient::Exception => e
75
+ response = e.response
76
+ rescue => e
77
+ raise
78
+ end
79
+ return JSON.parse(response) rescue {}
80
+ end
81
+
82
+ # Send a GET request and return the parsed JSON response.
83
+ #
84
+ # @example
85
+ # get('people', 'eric')
86
+ # get('people', {:name => 'eric'})
87
+ #
88
+ # @overload get(*path, params={})
89
+ # See {#request} for allowed parameters.
90
+ #
91
+ # @return [Hash]
92
+ # Parsed response, or an empty hash if parsing failed
93
+ #
94
+ def get(*path)
95
+ request('get', *path)
96
+ end
97
+
98
+ # Send a POST request and return the parsed JSON response.
99
+ #
100
+ # @example
101
+ # post('people', 'new', {:name => 'eric'})
102
+ #
103
+ # @overload post(*path, params={})
104
+ # See {#request} for allowed parameters.
105
+ #
106
+ # @return [Hash]
107
+ # Parsed response, or an empty hash if parsing failed
108
+ #
109
+ def post(*path)
110
+ request('post', *path)
111
+ end
112
+
113
+ # Send a PUT request and return the parsed JSON response.
114
+ #
115
+ # @example
116
+ # put('people', 'eric', {:title => 'Developer'})
117
+ #
118
+ # @overload put(*path, params={})
119
+ # See {#request} for allowed parameters.
120
+ #
121
+ # @return [Hash]
122
+ # Parsed response, or an empty hash if parsing failed
123
+ #
124
+ def put(*path)
125
+ request('put', *path)
126
+ end
127
+
128
+ # Send a DELETE request and return the parsed JSON response.
129
+ #
130
+ # @example
131
+ # delete('people', 'eric')
132
+ # delete('people', {:name => 'eric'})
133
+ #
134
+ # @overload delete(*path, params={})
135
+ # See {#request} for allowed parameters.
136
+ #
137
+ # @return [Hash]
138
+ # Parsed response, or an empty hash if parsing failed
139
+ #
140
+ def delete(*path)
141
+ request('delete', *path)
142
+ end
143
+
144
+ # Send a GET to a path that returns a single JSON object, and return the
145
+ # result as a Hashie::Mash.
146
+ #
147
+ # @overload get_mash(*path, params={})
148
+ # See {#request} for allowed parameters.
149
+ #
150
+ # @return [Hashie::Mash]
151
+ #
152
+ def get_mash(*path)
153
+ json = get(*path)
154
+ begin
155
+ return Hashie::Mash.new(json)
156
+ rescue => e
157
+ raise Placid::JSONParseError,
158
+ "Cannot parse JSON as key-value pairs: #{e.message}"
159
+ end
160
+ end
161
+
162
+ # Send a GET to a path that returns a JSON array of objects, and return the
163
+ # result as an array of Hashie::Mash objects.
164
+ #
165
+ # @overload get_mashes(*path, params={})
166
+ # See {#request} for allowed parameters.
167
+ #
168
+ # @return [Array]
169
+ #
170
+ def get_mashes(*path)
171
+ json = get(*path)
172
+ begin
173
+ return json.map {|rec| Hashie::Mash.new(rec)}
174
+ rescue => e
175
+ raise Placid::JSONParseError,
176
+ "Cannot parse JSON as an array of key-value pairs: #{e.message}"
177
+ end
178
+ end
179
+
180
+ end
181
+ end
182
+
@@ -0,0 +1,159 @@
1
+ require 'hashie'
2
+ require 'placid/helper'
3
+ require 'active_support/inflector' # for `pluralize` and `underscore`
4
+
5
+ module Placid
6
+ # Base class for RESTful models
7
+ class Model < Hashie::Mash
8
+
9
+ include Placid::Helper
10
+ extend Placid::Helper
11
+
12
+ # ------------------
13
+ # Instance methods
14
+ # ------------------
15
+
16
+ # Set the list of errors on this instance.
17
+ #
18
+ def errors=(new_errors)
19
+ self['errors'] = new_errors
20
+ end
21
+
22
+ # Return the list of errors on this instance. If the 'errors' attribute is
23
+ # either not set, or set to nil, then return an empty list.
24
+ #
25
+ def errors
26
+ return self['errors'] if self['errors']
27
+ return []
28
+ end
29
+
30
+ # Return true if there are any errors with this model.
31
+ #
32
+ def errors?
33
+ errors && !errors.empty?
34
+ end
35
+
36
+ # Return true if the given field is required.
37
+ #
38
+ def required?(field)
39
+ meta = self.class.meta
40
+ return meta[field] && meta[field][:required] == true
41
+ end
42
+
43
+ # Save this instance. This creates a new instance, or updates an existing
44
+ # one, with the attributes in this instance. Return true if creation or
45
+ # update were successful, false if there were any errors.
46
+ #
47
+ # @return [Boolean]
48
+ # true if save was successful, false if there were any errors
49
+ #
50
+ def save
51
+ existing = self.class.find(self.id)
52
+ if existing.nil?
53
+ obj = self.class.create(self.to_hash)
54
+ else
55
+ obj = self.class.update(self.id, self.to_hash)
56
+ end
57
+ self.merge!(obj)
58
+ return !errors?
59
+ end
60
+
61
+ # Return the value in the unique_id field.
62
+ #
63
+ def id
64
+ self[self.class.unique_id]
65
+ end
66
+
67
+
68
+ # ------------------
69
+ # Class methods
70
+ # ------------------
71
+
72
+ @unique_id = nil
73
+ @meta = nil
74
+
75
+ # Return the `snake_case` name of this model, based on the derived class
76
+ # name. This name should match the REST API path component used to interact
77
+ # with the corresponding model.
78
+ #
79
+ def self.model
80
+ return self.name.underscore
81
+ end
82
+
83
+ # Get or set the field name used for uniquely identifying instances of this
84
+ # model.
85
+ def self.unique_id(field=nil)
86
+ if field.nil?
87
+ return @unique_id || :id
88
+ else
89
+ @unique_id = field
90
+ end
91
+ end
92
+
93
+ # Return a Hashie::Mash of meta-data for this model.
94
+ #
95
+ def self.meta
96
+ @meta ||= get_mash(model, 'meta')
97
+ end
98
+
99
+ # Return a Hashie::Mash with a list of all model instances.
100
+ #
101
+ def self.list
102
+ get_mashes(model.pluralize)
103
+ end
104
+
105
+ # Return a Model instance matching the given id
106
+ #
107
+ # @param [String] id
108
+ # Identifier for the model instance to fetch
109
+ #
110
+ # @return [Model]
111
+ #
112
+ def self.find(id)
113
+ json = get(model, id)
114
+ return self.new(json)
115
+ end
116
+
117
+ # Create a new model instance and return it.
118
+ #
119
+ # @param [Hash] attrs
120
+ # Attribute values for the new instance
121
+ #
122
+ # @return [Model]
123
+ #
124
+ def self.create(attrs={})
125
+ obj = self.new(attrs)
126
+ json = post(model, attrs)
127
+ obj.merge!(json)
128
+ return obj
129
+ end
130
+
131
+ # Update an existing model instance.
132
+ #
133
+ # @param [String] id
134
+ # Identifier of the model instance to update
135
+ # @param [Hash] attrs
136
+ # New attribute values to set
137
+ #
138
+ # @return [Model]
139
+ #
140
+ def self.update(id, attrs={})
141
+ obj = self.new(attrs)
142
+ json = put(model, id, attrs)
143
+ obj.merge!(json)
144
+ #obj.errors = json['errors']
145
+ return obj
146
+ end
147
+
148
+ # Destroy a model instance.
149
+ #
150
+ # @param [String] id
151
+ # Identifier for the model instance to delete
152
+ #
153
+ def self.destroy(id)
154
+ delete(model, id)
155
+ end
156
+
157
+ end
158
+ end
159
+
data/lib/placid.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'placid/model'
2
+ require 'placid/helper'
3
+ require 'placid/config'
4
+
5
+ module Placid
6
+ end
data/placid.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "placid"
3
+ s.version = "0.0.1"
4
+ s.summary = "Models from REST"
5
+ s.description = <<-EOS
6
+ EOS
7
+ s.authors = ["Eric Pierce"]
8
+ s.email = "epierce@automation-excellence.com"
9
+ s.homepage = "http://github.com/a-e/placid"
10
+ s.platform = Gem::Platform::RUBY
11
+
12
+ s.add_dependency 'hashie'
13
+ s.add_dependency 'json'
14
+ s.add_dependency 'rest-client'
15
+ s.add_dependency 'activesupport'
16
+
17
+ s.add_development_dependency 'rspec'
18
+ s.add_development_dependency 'yard' # For documentation
19
+ s.add_development_dependency 'redcarpet' # For YARD / Markdown
20
+ s.add_development_dependency 'rcov'
21
+
22
+ s.files = `git ls-files`.split("\n")
23
+
24
+ s.require_path = 'lib'
25
+ end
@@ -0,0 +1,20 @@
1
+ require 'spec_helper'
2
+
3
+ describe Placid::Config do
4
+ after(:each) do
5
+ Placid::Config.rest_url = Placid::Config.default_url
6
+ end
7
+
8
+ it "has a default rest_url that is read-only" do
9
+ Placid::Config.rest_url.should == Placid::Config.default_url
10
+ lambda do
11
+ Placid::Config.default_url = 'foo'
12
+ end.should raise_error(NoMethodError)
13
+ end
14
+
15
+ it "allows setting rest_url" do
16
+ Placid::Config.rest_url = 'http://www.example.com'
17
+ Placid::Config.rest_url.should == 'http://www.example.com'
18
+ end
19
+ end
20
+
@@ -0,0 +1,109 @@
1
+ require 'spec_helper'
2
+
3
+ describe Placid::Helper do
4
+ describe "#escape" do
5
+ it "escapes all URI reserved characters" do
6
+ escape(";/?:@&=+$,[]").should == "%3B%2F%3F%3A%40%26%3D%2B%24%2C%5B%5D"
7
+ end
8
+ end
9
+
10
+ describe "#url" do
11
+ it "joins path components with '/'" do
12
+ url('foo', 'bar', 'baz').should == 'http://localhost/foo/bar/baz'
13
+ end
14
+
15
+ it "escapes path components to make them URI-safe" do
16
+ url('a b', 'c:d', 'e/f').should == 'http://localhost/a%20b/c%3Ad/e%2Ff'
17
+ end
18
+ end
19
+
20
+ describe "#request" do
21
+ it "returns a legitimate response as JSON" do
22
+ RestClient.stub(:get => '["success"]')
23
+ json = request('get')
24
+ json.should == ["success"]
25
+ end
26
+
27
+ it "returns a RestClient::Exception response as JSON" do
28
+ class RestException < RestClient::Exception
29
+ def response; '["fail"]'; end
30
+ end
31
+ RestClient.stub(:get).and_raise(RestException)
32
+ json = request('get')
33
+ json.should == ["fail"]
34
+ end
35
+
36
+ it "passes other exceptions through" do
37
+ RestClient.stub(:get).and_raise(URI::InvalidURIError)
38
+ lambda do
39
+ json = request('get')
40
+ end.should raise_error(URI::InvalidURIError)
41
+ end
42
+
43
+ it "returns an empty hash if there is no response" do
44
+ RestClient.stub(:get) { nil }
45
+ json = request('get')
46
+ json.should == {}
47
+ end
48
+
49
+ it "accepts a params hash as the last argument" do
50
+ RestClient.should_receive(:post).with('http://localhost/foo', {:bar => 'hi'})
51
+ json = request('post', 'foo', :bar => 'hi')
52
+ end
53
+
54
+ it "sends an empty params hash if none is given" do
55
+ RestClient.should_receive(:post).with('http://localhost/foo', {})
56
+ json = request('post', 'foo')
57
+ end
58
+
59
+ it "sends :params => params for get requests" do
60
+ RestClient.should_receive(:get).with('http://localhost/foo', {:params => {:x => 'y'}})
61
+ json = request('get', 'foo', :x => 'y')
62
+ end
63
+ end
64
+
65
+ describe "#get_mash" do
66
+ it "returns a Hashie::Mash for hash data" do
67
+ data = {
68
+ 'first_name' => 'Nathan',
69
+ 'last_name' => 'Stark',
70
+ }
71
+ RestClient.stub(:get => JSON(data))
72
+ mash = get_mash
73
+ mash.first_name.should == 'Nathan'
74
+ mash.last_name.should == 'Stark'
75
+ end
76
+
77
+ it "raises an exception when the JSON cannot be parsed" do
78
+ data = ['a', 'b', 'c']
79
+ RestClient.stub(:get => JSON(data))
80
+ lambda do
81
+ mashes = get_mash
82
+ end.should raise_error(Placid::JSONParseError)
83
+ end
84
+ end
85
+
86
+ describe "#get_mashes" do
87
+ it "returns a list of Hashie::Mash for list data" do
88
+ data = [
89
+ {'first_name' => 'Jack', 'last_name' => 'Carter'},
90
+ {'first_name' => 'Allison', 'last_name' => 'Blake'},
91
+ ]
92
+ RestClient.stub(:get => JSON(data))
93
+ mashes = get_mashes
94
+ mashes.first.first_name.should == 'Jack'
95
+ mashes.first.last_name.should == 'Carter'
96
+ mashes.last.first_name.should == 'Allison'
97
+ mashes.last.last_name.should == 'Blake'
98
+ end
99
+
100
+ it "raises an exception when the JSON cannot be parsed" do
101
+ data = ['a', 'b', 'c']
102
+ RestClient.stub(:get => JSON(data))
103
+ lambda do
104
+ mashes = get_mashes
105
+ end.should raise_error(Placid::JSONParseError)
106
+ end
107
+ end
108
+ end
109
+
@@ -0,0 +1,330 @@
1
+ require 'spec_helper'
2
+
3
+ describe Placid::Model do
4
+ class Thing < Placid::Model
5
+ end
6
+
7
+ context "Instance methods" do
8
+ describe "#id" do
9
+ it "returns the value in the custom unique ID field" do
10
+ class Person < Placid::Model
11
+ unique_id :email
12
+ end
13
+ person_1 = Person.new(:email => 'foo1@bar.com')
14
+ person_2 = Person.new(:email => 'foo2@bar.org')
15
+ person_1.id.should == 'foo1@bar.com'
16
+ person_2.id.should == 'foo2@bar.org'
17
+ end
18
+
19
+ it "returns the value in the :id field if no custom field was set" do
20
+ thing_1 = Thing.new(:id => '111')
21
+ thing_2 = Thing.new(:id => '222')
22
+ thing_1.id.should == '111'
23
+ thing_2.id.should == '222'
24
+ end
25
+ end
26
+
27
+ describe "#save" do
28
+ it "creates a new instance if one doesn't exist" do
29
+ thing = Thing.new(:id => '123')
30
+ Thing.stub(:find => nil)
31
+ Thing.should_receive(:create).
32
+ with({'id' => '123'}).
33
+ and_return(Thing.new)
34
+ thing.save
35
+ end
36
+
37
+ it "updates an existing instance" do
38
+ thing = Thing.new(:id => '123')
39
+ Thing.stub(:find => {:id => '123'})
40
+ Thing.should_receive(:update).
41
+ with('123', {'id' => '123'}).
42
+ and_return(Thing.new)
43
+ thing.save
44
+ end
45
+
46
+ it "merges saved attributes on create" do
47
+ thing = Thing.new(:id => '123')
48
+ saved_attribs = {'id' => '123', 'name' => 'foo'}
49
+ Thing.stub(:find => nil)
50
+ Thing.should_receive(:create).
51
+ with({'id' => '123'}).
52
+ and_return(Thing.new(saved_attribs))
53
+ thing.save
54
+ thing.should == saved_attribs
55
+ end
56
+
57
+ it "merges saved attributes on update" do
58
+ thing = Thing.new(:id => '123')
59
+ saved_attribs = {'id' => '123', 'name' => 'foo'}
60
+ Thing.stub(:find => {:id => '123'})
61
+ Thing.should_receive(:update).
62
+ with('123', {'id' => '123'}).
63
+ and_return(Thing.new(saved_attribs))
64
+ thing.save
65
+ thing.should == saved_attribs
66
+ end
67
+
68
+ it "returns false if errors were reported" do
69
+ thing = Thing.new
70
+ Thing.stub(:find => nil)
71
+ Thing.stub(:post => {'errors' => 'Missing id'})
72
+ thing.save.should be_false
73
+ end
74
+
75
+ it "returns true if errors is an empty list" do
76
+ thing = Thing.new(:id => '123')
77
+ Thing.stub(:find => nil)
78
+ Thing.stub(:post => {'errors' => []})
79
+ thing.save.should be_true
80
+ end
81
+
82
+ it "returns true if no errors were reported" do
83
+ thing = Thing.new(:id => '123')
84
+ Thing.stub(:find => nil)
85
+ Thing.stub(:post => {})
86
+ thing.save.should be_true
87
+ end
88
+ end
89
+
90
+ describe "#required?" do
91
+ it "true if the given field is implicitly required" do
92
+ Thing.stub(:meta => {:id => {:required => true}})
93
+ thing = Thing.new
94
+ thing.required?(:id).should == true
95
+ end
96
+
97
+ it "false if the given field is explicitly optional" do
98
+ Thing.stub(:meta => {:id => {:required => false}})
99
+ thing = Thing.new
100
+ thing.required?(:id).should == false
101
+ end
102
+
103
+ it "false if the given field is implicitly optional" do
104
+ Thing.stub(:meta => {:id => {}})
105
+ thing = Thing.new
106
+ thing.required?(:id).should == false
107
+ end
108
+ end
109
+
110
+ describe "#errors=" do
111
+ it "sets the list of errors on the instance" do
112
+ thing = Thing.new
113
+ thing.errors = ['missing id']
114
+ thing['errors'].should == ['missing id']
115
+ end
116
+ end
117
+
118
+ describe "#errors" do
119
+ it "returns errors set on initialization" do
120
+ thing = Thing.new(:errors => ['missing id'])
121
+ thing.errors = ['missing id']
122
+ thing.errors.should == ['missing id']
123
+ end
124
+
125
+ it "returns errors set after initialization" do
126
+ thing = Thing.new
127
+ thing.errors = ['missing id']
128
+ thing.errors.should == ['missing id']
129
+ end
130
+
131
+ it "returns [] if errors are not set" do
132
+ thing = Thing.new
133
+ thing.errors.should == []
134
+ end
135
+
136
+ it "returns [] if errors is set to nil" do
137
+ thing = Thing.new
138
+ thing.errors = nil
139
+ thing.errors.should == []
140
+ end
141
+ end
142
+
143
+ describe "#errors?" do
144
+ it "returns true if errors is set to a nonempty value" do
145
+ thing = Thing.new(:errors => ['missing id'])
146
+ thing.errors?.should be_true
147
+ end
148
+
149
+ it "returns false if errors it not set" do
150
+ thing = Thing.new
151
+ thing.errors?.should be_false
152
+ end
153
+
154
+ it "returns false if errors is set to nil" do
155
+ thing = Thing.new
156
+ thing.errors = nil
157
+ thing.errors?.should be_false
158
+ end
159
+
160
+ it "returns false if errors is set to an empty list" do
161
+ thing = Thing.new
162
+ thing.errors = []
163
+ thing.errors?.should be_false
164
+ end
165
+ end
166
+
167
+ describe "helpers" do
168
+ it "can call #get on an instance" do
169
+ thing = Thing.new
170
+ RestClient.should_receive(:get).
171
+ with('http://localhost/thing/foo', {:params => {:x => 'y'}})
172
+ thing.get('thing', 'foo', :x => 'y')
173
+ end
174
+ end
175
+ end
176
+
177
+ context "Class methods" do
178
+ describe "#unique_id" do
179
+ it "returns :id if no ID was set in the derived class" do
180
+ class Default < Placid::Model
181
+ end
182
+ Default.unique_id.should == :id
183
+ end
184
+
185
+ it "returns the unique ID that was set in the derived class" do
186
+ class Explicit < Placid::Model
187
+ unique_id :custom
188
+ end
189
+ Explicit.unique_id.should == :custom
190
+ end
191
+ end
192
+
193
+ describe "#model" do
194
+ it "converts CamelCase to snake_case" do
195
+ class MyModelName < Placid::Model
196
+ end
197
+ MyModelName.model.should == 'my_model_name'
198
+ end
199
+ end
200
+
201
+ describe "#meta" do
202
+ it "returns a Mash of model meta-data" do
203
+ thing_meta = {
204
+ 'name' => {'type' => 'String', 'required' => true}
205
+ }
206
+ RestClient.should_receive(:get).
207
+ with('http://localhost/thing/meta', {:params => {}}).
208
+ and_return(JSON(thing_meta))
209
+ Thing.meta.should == thing_meta
210
+ end
211
+
212
+ it "only sends a GET meta request once for the class" do
213
+ thing_meta = {
214
+ 'name' => {'type' => 'String', 'required' => true}
215
+ }
216
+ RestClient.stub(:get => JSON(thing_meta))
217
+ RestClient.should_receive(:get).at_most(:once)
218
+ Thing.meta.should == thing_meta
219
+ Thing.meta.should == thing_meta
220
+ Thing.meta.should == thing_meta
221
+ end
222
+
223
+ it "stores meta-data separately for each derived class" do
224
+ class ThingOne < Placid::Model; end
225
+ class ThingTwo < Placid::Model; end
226
+
227
+ thing_one_meta = {
228
+ 'one' => {'type' => 'String', 'required' => true}
229
+ }
230
+ RestClient.should_receive(:get).
231
+ with('http://localhost/thing_one/meta', {:params => {}}).
232
+ and_return(JSON(thing_one_meta))
233
+ ThingOne.meta.should == thing_one_meta
234
+
235
+ thing_two_meta = {
236
+ 'two' => {'type' => 'String', 'required' => false}
237
+ }
238
+ RestClient.should_receive(:get).
239
+ with('http://localhost/thing_two/meta', {:params => {}}).
240
+ and_return(JSON(thing_two_meta))
241
+ ThingTwo.meta.should == thing_two_meta
242
+ end
243
+ end
244
+
245
+ describe "#list" do
246
+ it "returns a Mash list of all model instances" do
247
+ data = [
248
+ {'name' => 'Foo'},
249
+ {'name' => 'Bar'},
250
+ ]
251
+ RestClient.stub(:get => JSON(data))
252
+ Thing.list.should == data
253
+ end
254
+ end
255
+
256
+ describe "#find" do
257
+ it "returns a Model instance matching the given id" do
258
+ data = {'name' => 'Foo'}
259
+ RestClient.stub(:get => JSON(data))
260
+ Thing.find(1).should == data
261
+ end
262
+ end
263
+
264
+ describe "#create" do
265
+ context "attributes include" do
266
+ it "posted attributes if no attributes were returned" do
267
+ RestClient.stub(:post => '{}')
268
+ attrs = {'name' => 'Foo'}
269
+ Thing.create(attrs).should == {'name' => 'Foo'}
270
+ end
271
+
272
+ it "returned attributes if no attributes were posted" do
273
+ RestClient.stub(:post => '{"uri": "foo"}')
274
+ attrs = {}
275
+ Thing.create(attrs).should == {'uri' => 'foo'}
276
+ end
277
+
278
+ it "original attributes merged with returned attributes" do
279
+ RestClient.stub(:post => '{"uri": "foo"}')
280
+ attrs = {'name' => 'Foo'}
281
+ Thing.create(attrs).should == {'name' => 'Foo', 'uri' => 'foo'}
282
+ end
283
+ end
284
+
285
+ it "sets errors on the Model instance" do
286
+ data = {'errors' => ['name is required']}
287
+ RestClient.stub(:post => JSON(data))
288
+ Thing.create().errors.should == ['name is required']
289
+ end
290
+ end
291
+
292
+ describe "#update" do
293
+ context "attributes include" do
294
+ it "posted attributes if no attributes were returned" do
295
+ RestClient.stub(:put => '{}')
296
+ attrs = {'name' => 'Foo'}
297
+ result = Thing.update(1, attrs)
298
+ Thing.update(1, attrs).should == {'name' => 'Foo'}
299
+ end
300
+
301
+ it "returned attributes if no attributes were posted" do
302
+ RestClient.stub(:put => '{"uri": "foo"}')
303
+ attrs = {}
304
+ Thing.update(1, attrs).should == {'uri' => 'foo'}
305
+ end
306
+
307
+ it "original attributes merged with returned attributes" do
308
+ RestClient.stub(:put => '{"uri": "foo"}')
309
+ attrs = {'name' => 'Foo'}
310
+ Thing.update(1, attrs).should == {'name' => 'Foo', 'uri' => 'foo'}
311
+ end
312
+ end
313
+
314
+ it "sets errors on the Model instance" do
315
+ data = {'errors' => ['name is required']}
316
+ RestClient.stub(:put => JSON(data))
317
+ Thing.update(1, {}).errors.should == ['name is required']
318
+ end
319
+ end
320
+
321
+ describe "#destroy" do
322
+ it "returns the parsed JSON response" do
323
+ data = {'status' => 'ok'}
324
+ RestClient.stub(:delete => JSON(data))
325
+ Thing.destroy(1).should == data
326
+ end
327
+ end
328
+ end
329
+ end
330
+
@@ -0,0 +1,12 @@
1
+ # This file includes RSpec configuration that is needed for all spec testing.
2
+
3
+ require 'rspec'
4
+ require 'rspec/autorun' # needed for RSpec 2.6.x
5
+ require 'placid'
6
+ require 'json'
7
+
8
+ RSpec.configure do |config|
9
+ config.color_enabled = true
10
+ config.include Placid
11
+ config.include Placid::Helper
12
+ end
metadata ADDED
@@ -0,0 +1,192 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: placid
3
+ version: !ruby/object:Gem::Version
4
+ hash: 29
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 1
10
+ version: 0.0.1
11
+ platform: ruby
12
+ authors:
13
+ - Eric Pierce
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-05-17 00:00:00 Z
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: hashie
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ hash: 3
29
+ segments:
30
+ - 0
31
+ version: "0"
32
+ type: :runtime
33
+ version_requirements: *id001
34
+ - !ruby/object:Gem::Dependency
35
+ name: json
36
+ prerelease: false
37
+ requirement: &id002 !ruby/object:Gem::Requirement
38
+ none: false
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ hash: 3
43
+ segments:
44
+ - 0
45
+ version: "0"
46
+ type: :runtime
47
+ version_requirements: *id002
48
+ - !ruby/object:Gem::Dependency
49
+ name: rest-client
50
+ prerelease: false
51
+ requirement: &id003 !ruby/object:Gem::Requirement
52
+ none: false
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ hash: 3
57
+ segments:
58
+ - 0
59
+ version: "0"
60
+ type: :runtime
61
+ version_requirements: *id003
62
+ - !ruby/object:Gem::Dependency
63
+ name: activesupport
64
+ prerelease: false
65
+ requirement: &id004 !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ hash: 3
71
+ segments:
72
+ - 0
73
+ version: "0"
74
+ type: :runtime
75
+ version_requirements: *id004
76
+ - !ruby/object:Gem::Dependency
77
+ name: rspec
78
+ prerelease: false
79
+ requirement: &id005 !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ hash: 3
85
+ segments:
86
+ - 0
87
+ version: "0"
88
+ type: :development
89
+ version_requirements: *id005
90
+ - !ruby/object:Gem::Dependency
91
+ name: yard
92
+ prerelease: false
93
+ requirement: &id006 !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ hash: 3
99
+ segments:
100
+ - 0
101
+ version: "0"
102
+ type: :development
103
+ version_requirements: *id006
104
+ - !ruby/object:Gem::Dependency
105
+ name: redcarpet
106
+ prerelease: false
107
+ requirement: &id007 !ruby/object:Gem::Requirement
108
+ none: false
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ hash: 3
113
+ segments:
114
+ - 0
115
+ version: "0"
116
+ type: :development
117
+ version_requirements: *id007
118
+ - !ruby/object:Gem::Dependency
119
+ name: rcov
120
+ prerelease: false
121
+ requirement: &id008 !ruby/object:Gem::Requirement
122
+ none: false
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ hash: 3
127
+ segments:
128
+ - 0
129
+ version: "0"
130
+ type: :development
131
+ version_requirements: *id008
132
+ description: ""
133
+ email: epierce@automation-excellence.com
134
+ executables: []
135
+
136
+ extensions: []
137
+
138
+ extra_rdoc_files: []
139
+
140
+ files:
141
+ - .gitignore
142
+ - .yardopts
143
+ - Gemfile
144
+ - MIT-LICENSE
145
+ - README.md
146
+ - Rakefile
147
+ - lib/placid.rb
148
+ - lib/placid/config.rb
149
+ - lib/placid/exceptions.rb
150
+ - lib/placid/helper.rb
151
+ - lib/placid/model.rb
152
+ - placid.gemspec
153
+ - spec/placid_config_spec.rb
154
+ - spec/placid_helper_spec.rb
155
+ - spec/placid_model_spec.rb
156
+ - spec/spec_helper.rb
157
+ homepage: http://github.com/a-e/placid
158
+ licenses: []
159
+
160
+ post_install_message:
161
+ rdoc_options: []
162
+
163
+ require_paths:
164
+ - lib
165
+ required_ruby_version: !ruby/object:Gem::Requirement
166
+ none: false
167
+ requirements:
168
+ - - ">="
169
+ - !ruby/object:Gem::Version
170
+ hash: 3
171
+ segments:
172
+ - 0
173
+ version: "0"
174
+ required_rubygems_version: !ruby/object:Gem::Requirement
175
+ none: false
176
+ requirements:
177
+ - - ">="
178
+ - !ruby/object:Gem::Version
179
+ hash: 3
180
+ segments:
181
+ - 0
182
+ version: "0"
183
+ requirements: []
184
+
185
+ rubyforge_project:
186
+ rubygems_version: 1.8.23
187
+ signing_key:
188
+ specification_version: 3
189
+ summary: Models from REST
190
+ test_files: []
191
+
192
+ has_rdoc: