placid 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: