collection_json 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -7,3 +7,6 @@ vendor/gems/*
7
7
  .sass-cache/*
8
8
  *.swp
9
9
  **/*.swp
10
+ pkg
11
+ Gemfile.lock
12
+ .rvmrc
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --require spec_helper
2
+ –order rand
data/Guardfile ADDED
@@ -0,0 +1,9 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec', :version => 2 do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
9
+
data/README.md CHANGED
@@ -1,9 +1,76 @@
1
1
  # CollectionJson
2
2
 
3
- A gem to help with producing Hypermedia APIs with a MIME type of
3
+ An experimental gem to help with producing Hypermedia APIs with a MIME type of
4
4
  'application/vnd.collection+json'
5
5
 
6
- see http://amundsen.com/media-types/collection/format/#link-relations
6
+ see http://amundsen.com/media-types/collection/format/
7
+
8
+
9
+ ## Usage
10
+
11
+ The aim is to include the gem in a Rack app to generate hypermedia APIs from
12
+ your resources, effectively a hypermedia decorator.
13
+
14
+ Here's a preview of what you should be able to do in a Rails app:
15
+
16
+
17
+ ![Spider cow](https://github.com/markburns/collection_json/raw/master/doc/spider-cow.jpg)
18
+
19
+ ```ruby
20
+ class SpiderCowController < ApplicationController
21
+ respond_to :collection_json
22
+
23
+ def index
24
+ @spider_cows = SpiderCowDecorator.all do |collection, item|
25
+ collection.href "http://www.youtube.com/watch?v=FavUpD_IjVY"
26
+
27
+ item.links [{href: spider_cow_path(item), rel: "self"},
28
+ {href: spider_cow_path(item.father), rel: "father"}]
29
+ end
30
+
31
+ respond_with @spider_cows
32
+ end
33
+ end
34
+
35
+ class SpiderCow < ActiveRecord::Base
36
+ #attributes - legs, eyes, udders
37
+ end
38
+
39
+ class SpiderCowDecorator
40
+ extend CollectionJson::Decorator
41
+ end
42
+ ```
43
+
44
+ Sample output:
45
+
46
+ ```javascript
47
+ {"collection" :
48
+ {
49
+ "version" :"1.0",
50
+ "href" :"http://example.org/spider_cows/",
51
+
52
+ "links" :[ {"rel" :"father", "href" :"http://example.com/spider_cows/tom"}],
53
+
54
+ "items" :
55
+ [
56
+ {"data" :
57
+ [{"name" :"legs", "value" :7},
58
+ {"name" :"eyes", "value" :8},
59
+ {"name" :"udders", "value" :"blue"}
60
+ ]
61
+ },
62
+
63
+ {"data" :
64
+ [{"name" :"legs", "value" :6},
65
+ {"name" :"udders", "value" :"red"}
66
+ ]
67
+ }
68
+ ]
69
+ }
70
+ }
71
+
72
+ ```
73
+
7
74
 
8
75
  ## Installation
9
76
 
@@ -19,26 +86,25 @@ Or install it yourself as:
19
86
 
20
87
  $ gem install collection_json
21
88
 
22
- ## Usage
23
89
 
24
- This is still experimental at the moment.
25
- The aim is to be able to include the gem in a Rack app and make for easy
26
- generation of hypermedia APIs from your resources, effectively something like a
27
- hypermedia presenter.
90
+ ##Notes
28
91
 
29
- I'm not clear on the details of how the objects will be presented as
30
- Collection+JSON as there is an emphasis on Collections rather than individual
31
- items. This makes sense if you think of Rails controllers being plural.
92
+ Collection+JSON specifies the following concepts
32
93
 
33
- But it is reasonably different to being used to having the primary unit of data
34
- being a singular model.
94
+ Create, Read, Update, Delete, Template and Query
95
+ which correspond to the ideas in Rails like so:
35
96
 
36
- ```ruby
37
- SingularActiveRecordObject.all
38
-
39
- #versus:
40
- PluralJsonCollection.new collection_of_objects
41
- ```
97
+ <table>
98
+ <tr>
99
+ <th>Verb</th><th>Rails</th><th>Collection+JSON</th>
100
+ </tr>
101
+ <tr><td>POST </td><td>create </td><td>create</td></tr>
102
+ <tr><td>GET </td><td>show </td><td>read</td></tr>
103
+ <tr><td>PUT </td><td>update </td><td>update</td></tr>
104
+ <tr><td>DELETE</td><td>destroy </td><td>delete</td></tr>
105
+ <tr><td>GET </td><td>edit/new</td><td>template</td></tr>
106
+ <tr><td>GET </td><td>index </td><td>query</td></tr>
107
+ </table>
42
108
 
43
109
  ## Contributing
44
110
 
@@ -47,3 +113,6 @@ being a singular model.
47
113
  3. Commit your changes (`git commit -am 'Added some feature'`)
48
114
  4. Push to the branch (`git push origin my-new-feature`)
49
115
  5. Create new Pull Request
116
+
117
+
118
+
@@ -1,5 +1,5 @@
1
1
  # -*- encoding: utf-8 -*-
2
- require File.expand_path('../lib/collection_json/version', __FILE__)
2
+ require File.expand_path('../lib/collection_json/gem_version', __FILE__)
3
3
 
4
4
  Gem::Specification.new do |gem|
5
5
  gem.authors = ["Mark Burns"]
@@ -7,11 +7,24 @@ Gem::Specification.new do |gem|
7
7
  gem.description = %q{Help create Collection+JSON hypermedia APIs}
8
8
  gem.summary = %q{As specified by: http://amundsen.com/media-types/collection/format/#objects}
9
9
  gem.homepage = "https://github.com/markburns/collection_json"
10
+ gem.add_dependency 'virtus'
11
+ gem.add_dependency 'activesupport'
12
+ gem.add_dependency 'i18n'
13
+ gem.add_dependency 'draper'
14
+ gem.add_dependency 'funky_accessor'
15
+
16
+ gem.add_development_dependency 'guard-rspec'
17
+ gem.add_development_dependency 'growl'
18
+ gem.add_development_dependency 'rspec'
19
+ gem.add_development_dependency 'ruby-debug19'
20
+ gem.add_development_dependency 'ruby-debug-base19', '0.11.26'
21
+ gem.add_development_dependency 'linecache19', '0.5.13'
22
+ gem.add_development_dependency 'awesome_print'
10
23
 
11
24
  gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
12
25
  gem.files = `git ls-files`.split("\n")
13
26
  gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
14
27
  gem.name = "collection_json"
15
28
  gem.require_paths = ["lib"]
16
- gem.version = CollectionJson::VERSION
29
+ gem.version = CollectionJson::GEM_VERSION
17
30
  end
@@ -0,0 +1,36 @@
1
+ collection = new Collection 'api.spider-cows.com'
2
+
3
+ console.log collection.response
4
+
5
+
6
+ {"collection" :
7
+ {
8
+ "version" :"1.0",
9
+ "href" :"http://api.spider-cows.com/",
10
+
11
+ "links" :[ {"rel" :"boss_spider", "href" :"http://example.com/spider_cows/tom"}],
12
+
13
+ "items" :
14
+ [
15
+ {"data" :
16
+ [{"name" :"legs", "value" :7},
17
+ {"name" :"eyes", "value" :8},
18
+ {"name" :"udders", "value" :"blue"}
19
+ ]
20
+ },
21
+
22
+ {"data" :
23
+ [{"name" :"legs", "value" :6},
24
+ {"name" :"udders", "value" :"red"}
25
+ ]
26
+ }
27
+ ]
28
+ }
29
+ }
30
+
31
+
32
+ console.log collection.links
33
+ [{"rel" :"boss_spider", "href" :"http://example.com/spider_cows/tom"}]
34
+
35
+
36
+ console.log collection.links
@@ -0,0 +1,66 @@
1
+ {"collection" :
2
+ {
3
+ "version" : "1.0",
4
+ "href" : "http://example.org/friends/",
5
+
6
+ "links" : [
7
+ {"rel" : "feed", "href" : "http://example.org/friends/rss"}
8
+ ],
9
+
10
+ "items" : [
11
+ {
12
+ "href" : "http://example.org/friends/jdoe",
13
+ "data" : [
14
+ {"name" : "full-name", "value" : "J. Doe", "prompt" : "Full Name"},
15
+ {"name" : "email", "value" : "jdoe@example.org", "prompt" : "Email"}
16
+ ],
17
+ "links" : [
18
+ {"rel" : "blog", "href" : "http://examples.org/blogs/jdoe", "prompt" : "Blog"},
19
+ {"rel" : "avatar", "href" : "http://examples.org/images/jdoe", "prompt" : "Avatar", "render" : "image"}
20
+ ]
21
+ },
22
+
23
+ {
24
+ "href" : "http://example.org/friends/msmith",
25
+ "data" : [
26
+ {"name" : "full-name", "value" : "M. Smith", "prompt" : "Full Name"},
27
+ {"name" : "email", "value" : "msmith@example.org", "prompt" : "Email"}
28
+ ],
29
+ "links" : [
30
+ {"rel" : "blog", "href" : "http://examples.org/blogs/msmith", "prompt" : "Blog"},
31
+ {"rel" : "avatar", "href" : "http://examples.org/images/msmith", "prompt" : "Avatar", "render" : "image"}
32
+ ]
33
+ },
34
+
35
+ {
36
+ "href" : "http://example.org/friends/rwilliams",
37
+ "data" : [
38
+ {"name" : "full-name", "value" : "R. Williams", "prompt" : "Full Name"},
39
+ {"name" : "email", "value" : "rwilliams@example.org", "prompt" : "Email"}
40
+ ],
41
+ "links" : [
42
+ {"rel" : "blog", "href" : "http://examples.org/blogs/rwilliams", "prompt" : "Blog"},
43
+ {"rel" : "avatar", "href" : "http://examples.org/images/rwilliams", "prompt" : "Avatar", "render" : "image"}
44
+ ]
45
+ }
46
+ ],
47
+
48
+ "queries" : [
49
+ {"rel" : "search", "href" : "http://example.org/friends/search", "prompt" : "Search",
50
+ "data" : [
51
+ {"name" : "search", "value" : ""}
52
+ ]
53
+ }
54
+ ],
55
+
56
+ "template" : {
57
+ "data" : [
58
+ {"name" : "full-name", "value" : "", "prompt" : "Full Name"},
59
+ {"name" : "email", "value" : "", "prompt" : "Email"},
60
+ {"name" : "blog", "value" : "", "prompt" : "Blog"},
61
+ {"name" : "avatar", "value" : "", "prompt" : "Avatar"}
62
+
63
+ ]
64
+ }
65
+ }
66
+ }
Binary file
Binary file
@@ -1,84 +1,42 @@
1
+ require "active_support/dependencies/autoload"
2
+ require "active_support/version"
3
+ require "delegate"
1
4
  require 'json'
5
+ require 'virtus'
6
+
7
+ require 'active_support/lazy_load_hooks'
8
+ require 'active_support/concern'
2
9
  require 'active_support/core_ext/string'
3
- require 'collection_json/version'
10
+ require 'active_support/core_ext/hash'
11
+ require 'collection_json/gem_version'
12
+ require 'draper'
13
+
14
+ require 'active_support/deprecation'
15
+ require 'active_support/concern'
16
+ require 'virtus'
17
+ require 'virtus/attribute'
18
+ require 'funky_accessor'
4
19
 
5
20
  #see http://amundsen.com/media-types/collection/format/#link-relations
6
21
 
7
22
  # 2.1. collection
8
23
  # The collection object contains all the "records" in the representation.
9
- # This is a REQUIRED object and there MUST NOT be more than one collection object in a Collection+JSON document.
24
+ # This is a REQUIRED object and there MUST NOT be more than one collection
25
+ # object in a Collection+JSON document.
10
26
  # It is a top-level document property.
11
27
  module CollectionJson
12
- attr_writer :href, :links, :items, :queries, :template
13
-
14
- def to_json
15
- {collection: collection}.to_json
16
- end
17
-
18
- def collection
19
- { version: "1.0", #The collection object SHOULD have a version property. For this release,
20
- #the value of the version property MUST be set to 1.0.
21
- #If there is no version property present, it should be assumed to be set to 1.0.
22
-
23
- href: href, #The collection object SHOULD have an href property.
24
- #The href property MUST contain a valid URI.
25
- #This URI SHOULD represent the address used to retrieve a representation of the document.
26
- #This URI MAY be used to add a new record
27
-
28
- links: links, #The collection object MAY have an links array child property
29
-
30
- items: items, #The collection object MAY have an items array child property.
31
- #Each item in a Collection+JSONcollection has an assigned URI (via the href property) and an optional array of one or more data elements along with an optional array of one or more link elements.
32
- queries: queries, #The collection object MAY have an queries array child property.
33
-
34
- template: template, #The collection object MAY have an template object child property.
35
- error: error #The collection object MAY have an error object child property.
36
- }
37
- end
38
-
39
- def href
40
- @href || "/#{self.class.to_s.underscore.pluralize}"
41
- end
42
-
43
- #3.4 links
44
- #The links array is an OPTIONAL child property of the items array
45
- def links
46
- #The Collection+JSON hypermedia type has a limited set of predefined link
47
- #relation values and supports additional values applied by implementors
48
- #in order to better describe the application domain to which the media
49
- #type is applied.
50
- @links ||= []
51
- end
52
-
53
- def items
54
- @items
55
- end
56
-
57
- #3.3. queries
58
- #The queries array is an OPTIONAL top-level property of the Collection+JSON document.
59
- def queries
60
- @queries ||= []
61
- end
28
+ extend ActiveSupport::Autoload
62
29
 
63
- #The template object contains all the input elements used to add or edit collection "records." This is an OPTIONAL object and there MUST NOT be more than one template object in a Collection+JSON document. It is a top-level document property.
64
- #The template object SHOULD have a data array child property.
65
- def template
66
- @template ||= {}
67
- end
30
+ autoload :Rack
31
+ autoload :Collection
32
+ autoload :Link
33
+ autoload :Item
34
+ autoload :Query
35
+ autoload :Template
68
36
 
69
- #2.2. error
70
- #The error object contains addiitional information on the latest error
71
- #condition reported by the server.
72
- #This is an OPTIONAL object and there MUST NOT be more than one error
73
- #object in a Collection+JSON document.
74
- #
75
- #It is a top-level document property.
76
- #
77
- #The following elements MAY appear as child properties of the error object:
78
- #code message and title.
79
- Error = Struct.new :code, :message, :title
37
+ autoload :InvalidJsonError, 'collection_json/exceptions'
38
+ autoload :InvalidUriError, 'collection_json/exceptions'
39
+ autoload :IncompatibleItem, 'collection_json/exceptions'
80
40
 
81
- def error
82
- @error
83
- end
41
+ autoload :Decorator
84
42
  end
@@ -0,0 +1,34 @@
1
+ module CollectionJson
2
+ class Collection
3
+ extend FunkyAccessor
4
+ funky_accessor :items, :version, :template, :href, :links
5
+
6
+ def initialize items
7
+ @version = "1.0"
8
+ @items = items.map { |i| Item.new(i) }
9
+ @template = @items.first.blank_template if @items.any?
10
+ @links = []
11
+ end
12
+
13
+ def representation
14
+ collection = {
15
+ version: version,
16
+ href: href,
17
+ links: links,
18
+ items: item_representations
19
+ }
20
+
21
+ collection.merge!({template: {data: template}}) if template
22
+
23
+ {collection: collection}
24
+ end
25
+
26
+ def to_json
27
+ representation.to_json
28
+ end
29
+
30
+ def item_representations
31
+ items.map &:singular_representation
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,13 @@
1
+ module CollectionJson::Decorator
2
+ def decorate object
3
+ klass = object.respond_to?(:each) ? CollectionJson::Collection : CollectionJson::Item
4
+
5
+ klass.new(object).tap do |o|
6
+ if object.respond_to? :each
7
+ o.items.each { |i| yield o, i } if block_given?
8
+ else
9
+ yield o if block_given?
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ module CollectionJson
2
+ InvalidJsonError = Class.new Exception
3
+ InvalidUriError = Class.new Exception
4
+ IncompatibleItem = Class.new Exception
5
+ end
@@ -1,4 +1,3 @@
1
1
  module CollectionJson
2
- module Query
3
- end
2
+ GEM_VERSION = "0.0.2"
4
3
  end
@@ -0,0 +1,60 @@
1
+ #The collection object MAY have an items array child property.
2
+ #Each item in a Collection+JSONcollection has an assigned URI
3
+ #(via the href property) and an optional array of one or more data
4
+ #elements along with an optional array of one or more link elements.
5
+ module CollectionJson
6
+ class Item < SimpleDelegator
7
+ extend FunkyAccessor
8
+ funky_accessor :href, :links, :version, :object
9
+
10
+ def initialize object
11
+ @object = object
12
+ pre_check
13
+ super object
14
+
15
+ @links = []
16
+ @version = "1.0"
17
+ end
18
+
19
+ def to_json
20
+ representation.to_json
21
+ end
22
+
23
+ def blank_template
24
+ template.map{|h| h[:value]=""; h }
25
+ end
26
+
27
+ def template
28
+ attributes
29
+ end
30
+
31
+ def singular_representation
32
+ item = {}
33
+ item.merge!({href: href}) if href
34
+ item.merge!({data: attributes}) if attributes.any?
35
+ end
36
+
37
+ def representation
38
+ r = { version: version}
39
+
40
+ r.merge!({href: href}) if href
41
+ r.merge!({links: links}) if links
42
+ r.merge!({items: [singular_representation]})
43
+
44
+ { collection: r }
45
+ end
46
+
47
+ def attributes
48
+ @attributes = @object.attributes || {}
49
+ @attributes.map{|k,v| {name: k.to_s, value: v}}
50
+ end
51
+
52
+ private
53
+
54
+ def pre_check
55
+ unless object.respond_to? :attributes
56
+ raise CollectionJson::IncompatibleItem.new("Decorated items must have an attributes method")
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,6 @@
1
+ module CollectionJson::Rack
2
+ extend ActiveSupport::Autoload
3
+
4
+ autoload :Parser
5
+ autoload :Interactions
6
+ end
@@ -0,0 +1,15 @@
1
+ module CollectionJson::Rack
2
+ class Parser
3
+ attr_reader :json
4
+
5
+ def initialize json_string
6
+ @json = JSON.parse json_string
7
+ rescue JSON::ParserError => e
8
+ raise CollectionJson::InvalidJsonError.new "Couldn't parse: <#{json_string}>, #{e.message}"
9
+ end
10
+
11
+ def valid?
12
+ false
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ #The template object contains all the input elements used to add or edit
2
+ #collection "records." This is an OPTIONAL object and there MUST NOT be more
3
+ #than one template object in a Collection+JSON document.
4
+ #It is a top-level document property.
5
+ #The template object SHOULD have a data array child property.
6
+ module CollectionJson
7
+ class Template
8
+ def initialize *attributes
9
+ @attributes = attributes
10
+ end
11
+
12
+ def to_json
13
+ @attributes.map{|a| {name: a, value: ""}}.to_json
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,109 @@
1
+ class SpiderCowDecorator ;extend CollectionJson::Decorator end
2
+
3
+ describe CollectionJson::Collection do
4
+ def cow(id) mock 'spider_cow', link: id, attributes: {name: "cow_#{id}"} end
5
+
6
+ let(:items) {[cow(1), cow(2), cow(3)]}
7
+
8
+ let(:links) {[{rel: "next", href: "spider_cows/?page=3"}] }
9
+
10
+ #crazy archaic plural I'd never heard of, cow -> kine. Thanks ActiveSupport
11
+ let(:href) { "/spider_kine" }
12
+
13
+ let(:spider_cows) do
14
+ SpiderCowDecorator.decorate items
15
+ end
16
+
17
+ describe "#href" do
18
+ specify do
19
+ spider_cows.href "/spider_cows"
20
+ spider_cows.href.should == "/spider_cows"
21
+ end
22
+ end
23
+
24
+ describe "#links" do
25
+ specify do
26
+ spider_cows.links %w(egg banana cheese)
27
+ spider_cows.links.should == %w(egg banana cheese)
28
+ end
29
+ end
30
+
31
+ describe "#representation" do
32
+ specify "has items" do
33
+ spider_cows.representation[:collection][:items].should ==
34
+ [{data: [{:name=>"name", :value=>"cow_1"}]},
35
+ {data: [{:name=>"name", :value=>"cow_2"}]},
36
+ {data: [{:name=>"name", :value=>"cow_3"}]}]
37
+ end
38
+
39
+ specify "has links" do
40
+ spider_cows.links links
41
+ spider_cows.representation[:collection][:links].should == links
42
+ end
43
+
44
+ specify "has href" do
45
+ spider_cows.href href
46
+ spider_cows.representation[:collection][:href].should == href
47
+ end
48
+ end
49
+
50
+ let(:banana_fish) do
51
+ BananaFish.new legs: 7, eyes: 8, color: "blue"
52
+ end
53
+
54
+ let(:banana_fish_2) do
55
+ BananaFish.new legs: 6, face: "red"
56
+ end
57
+
58
+ describe ".decorate_collection" do
59
+ let(:expected_2) do
60
+ {"collection" =>{
61
+ "version" => "1.0",
62
+ "href" => "http://example.org/banana_fish/",
63
+
64
+ "links" => [
65
+ {"rel" => "father", "href" => "http://example.com/bananas/tom"},
66
+ ],
67
+
68
+ "template" => {"data" =>
69
+ [{"name"=>"legs", "value"=>""},
70
+ {"name"=>"eyes", "value"=>""},
71
+ {"name"=>"color", "value"=>""}]
72
+ },
73
+ "items" =>
74
+ [
75
+ {"data" =>
76
+ [{"name" => "legs", "value" => 7},
77
+ {"name" => "eyes", "value" => 8},
78
+ {"name" => "color", "value" => "blue"}
79
+ ]
80
+ },
81
+
82
+ {"data" =>
83
+ [{"name" => "legs", "value" => 6},
84
+ {"name" => "face", "value" => "red"}
85
+ ]
86
+ }
87
+ ]
88
+ }
89
+ }
90
+ end
91
+ let(:collection) do
92
+ BananaFishDecorator.decorate [banana_fish, banana_fish_2] do |c,i|
93
+ c.href "http://example.org/banana_fish/"
94
+
95
+ c.links [ {"rel" => "father", "href" => "http://example.com/bananas/tom"} ]
96
+ end
97
+ end
98
+
99
+ let(:parsed) do
100
+ JSON[collection.to_json]
101
+ end
102
+
103
+ %w(version href links template).each do |a|
104
+ specify ", #{a} should match"do
105
+ parsed["collection"][a].should == expected_2["collection"][a]
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,39 @@
1
+ require File.expand_path('spec/spec_helper')
2
+ class BananaFish < OpenStruct
3
+ def attributes
4
+ @table
5
+ end
6
+ end
7
+
8
+ class BananaFishDecorator
9
+ extend CollectionJson::Decorator
10
+ end
11
+
12
+
13
+ describe CollectionJson::Decorator do
14
+ context "with a block" do
15
+ class CatDecorator
16
+ extend CollectionJson::Decorator
17
+ end
18
+
19
+ before do
20
+ cat = {legs: 4, tail: true, id: 1}
21
+ attributes = cat.merge({attributes: cat})
22
+
23
+ cats = [OpenStruct.new(attributes)]
24
+
25
+ decorated = CatDecorator.decorate(cats) do |collection, item|
26
+ collection.links << "/cats"
27
+ collection.href "/cats"
28
+ item.links << "/cats/#{item.id}"
29
+ item.href "/cats/#{item.id}"
30
+ end
31
+
32
+ @decorated = JSON[decorated.to_json]["collection"]
33
+ end
34
+
35
+ specify { @decorated["links"].should == ["/cats"] }
36
+ specify { @decorated["href"].should == "/cats" }
37
+ specify { @decorated["items"][0]["href"].should == "/cats/1" }
38
+ end
39
+ end
@@ -0,0 +1,28 @@
1
+ require './spec/support/sample_mvc'
2
+
3
+ describe "CollectionJson", type: :integration do
4
+ let(:controller) { SpiderCowController.new }
5
+
6
+ describe "controller#index" do
7
+ before do
8
+ controller.index
9
+ @spider_cows = controller.instance_eval{@spider_cows}
10
+ end
11
+
12
+ it "assigns a collection with items" do
13
+ items = @spider_cows.items
14
+ items.should be_a Enumerable
15
+
16
+ items.each do |x|
17
+ x.should be_a CollectionJson::Item
18
+ end
19
+ end
20
+
21
+ it "defines the link with a block" do
22
+ items = @spider_cows.items
23
+ item = items.first
24
+
25
+ item.links.should == [{href: "/spider_cows/1", rel: "self"}]
26
+ end
27
+ end
28
+ end
data/spec/item_spec.rb ADDED
@@ -0,0 +1,103 @@
1
+ class BananaFish < OpenStruct
2
+ def attributes
3
+ @table
4
+ end
5
+ end
6
+
7
+ class BananaFishDecorator
8
+ extend CollectionJson::Decorator
9
+ end
10
+
11
+
12
+ describe CollectionJson::Item do
13
+ let(:banana_fish) do
14
+ BananaFish.new legs: 7, eyes: 8, color: "blue"
15
+ end
16
+
17
+ let(:banana_fish_2) do
18
+ BananaFish.new legs: 6, face: "red"
19
+ end
20
+
21
+ describe ".decorate_item" do
22
+ let(:decorated) do
23
+ BananaFishDecorator.decorate banana_fish do |b|
24
+ b.href "http://example.org/banana_fish/1"
25
+ b.links [{"rel" => "father", "href" => "http://example.com/bananas/tom"},
26
+ {"rel" => "mother", "href" => "http://example.com/fish/tina"}]
27
+ end
28
+
29
+ end
30
+
31
+ let(:decorated_2) do
32
+ BananaFishDecorator.decorate banana_fish_2 do |b|
33
+ b.href "http://example.org/banana_fish/2"
34
+ b.links [{"rel" => "father", "href" => "http://example.com/bananas/tom"}]
35
+ end
36
+
37
+ end
38
+ let(:expected) do
39
+ {"collection" =>
40
+ {
41
+ "version" => "1.0",
42
+ "href" => "http://example.org/banana_fish/1",
43
+
44
+ "links" => [
45
+ {"rel" => "father", "href" => "http://example.com/bananas/tom"},
46
+ {"rel" => "mother", "href" => "http://example.com/fish/tina"}],
47
+
48
+ "items" =>
49
+ [{
50
+ "href"=>"http://example.org/banana_fish/1",
51
+ "data" =>
52
+
53
+ [{"name" => "legs", "value" => 7},
54
+ {"name" => "eyes", "value" => 8},
55
+ {"name" => "color", "value" => "blue"}
56
+ ]
57
+ }]}
58
+ }
59
+ end
60
+
61
+ let(:expected_2) do
62
+ {"collection" =>
63
+ {
64
+ "version" => "1.0",
65
+ "href" => "http://example.org/banana_fish/2",
66
+
67
+ "links" => [
68
+ {"rel" => "father", "href" => "http://example.com/bananas/tom"},
69
+ ],
70
+
71
+ "items" =>
72
+ [
73
+ {
74
+ "href"=>"http://example.org/banana_fish/1",
75
+ "data" =>
76
+ [{"name" => "legs", "value" => 6},
77
+ {"name" => "face", "value" => "red"}
78
+ ]
79
+ }
80
+ ]
81
+ }}
82
+ end
83
+
84
+ before do
85
+ decorated_json = decorated.to_json
86
+ decorated_json_2 = decorated_2.to_json
87
+
88
+ @parsed = JSON.parse(decorated_json )
89
+ @parsed_2 = JSON.parse(decorated_json_2)
90
+ end
91
+
92
+ specify { @parsed.keys.should == expected.keys }
93
+ specify { @parsed.values.should == expected.values }
94
+
95
+ %w(version href link items).each do |a|
96
+ specify ", #{a} should match" do
97
+ @parsed[a].should == expected[a]
98
+ @parsed_2[a].should == expected_2[a]
99
+ end
100
+ end
101
+ end
102
+
103
+ end
@@ -0,0 +1,23 @@
1
+ describe CollectionJson::Rack::Parser do
2
+ let(:parser) do
3
+ CollectionJson::Rack::Parser.new '{"a": 1, "c": "1"}'
4
+ end
5
+
6
+ describe "#initialize" do
7
+ specify do
8
+ ->{CollectionJson::Rack::Parser.new ""}.should raise_error CollectionJson::InvalidJsonError
9
+ end
10
+ end
11
+
12
+ describe "#json" do
13
+ specify do
14
+ parser.json.should == {"a" => 1, "c" => "1"}
15
+ end
16
+ end
17
+
18
+ describe "#valid?" do
19
+ specify do
20
+ parser.should_not be_valid
21
+ end
22
+ end
23
+ end
data/spec/spec_helper.rb CHANGED
@@ -4,10 +4,11 @@
4
4
  # loaded once.
5
5
  #
6
6
  # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
- Dir['./lib/**/*.rb'].each {|r| require r}
7
+ require 'collection_json'
8
+ require 'ruby-debug'
8
9
 
9
10
  RSpec.configure do |config|
10
11
  config.treat_symbols_as_metadata_keys_with_true_values = true
11
12
  config.run_all_when_everything_filtered = true
12
- config.filter_run :focus
13
+ #config.filter_run :focus
13
14
  end
@@ -0,0 +1,38 @@
1
+ class SpiderCowController
2
+ def index
3
+ spider_cows = SpiderCow.all
4
+
5
+ @spider_cows = SpiderCowDecorator.decorate(spider_cows) do |collection, item|
6
+ item.links [{href: spider_cow_path(item), rel: "self"}]
7
+ end
8
+ end
9
+
10
+ #in real life this method will be made available
11
+ #rather than defined here
12
+ def spider_cow_path cow
13
+ "/spider_cows/#{cow.id}"
14
+ end
15
+ end
16
+
17
+ class SpiderCow
18
+ attr_accessor :id
19
+
20
+ def self.all
21
+ [SpiderCow.new(1), SpiderCow.new(2), SpiderCow.new(3)]
22
+ end
23
+
24
+ def attributes
25
+ {id: @id}
26
+ end
27
+
28
+ def initialize id
29
+ @id = id
30
+ end
31
+ end
32
+
33
+
34
+ class SpiderCowDecorator
35
+ include CollectionJson::Decorator
36
+ end
37
+
38
+
@@ -0,0 +1,12 @@
1
+ require File.expand_path('spec/spec_helper')
2
+
3
+ describe CollectionJson::Template do
4
+ let(:template) {CollectionJson::Template.new(:legs, :eyes)}
5
+
6
+ specify do
7
+ template.to_json.should == [
8
+ {name: :legs, value: ""},
9
+ {name: :eyes, value: ""},
10
+ ].to_json
11
+ end
12
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: collection_json
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,8 +9,140 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-02-25 00:00:00.000000000 Z
13
- dependencies: []
12
+ date: 2012-03-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: virtus
16
+ requirement: &70360322207560 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70360322207560
25
+ - !ruby/object:Gem::Dependency
26
+ name: activesupport
27
+ requirement: &70360322206980 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70360322206980
36
+ - !ruby/object:Gem::Dependency
37
+ name: i18n
38
+ requirement: &70360322206380 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70360322206380
47
+ - !ruby/object:Gem::Dependency
48
+ name: draper
49
+ requirement: &70360322205860 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *70360322205860
58
+ - !ruby/object:Gem::Dependency
59
+ name: funky_accessor
60
+ requirement: &70360322205100 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :runtime
67
+ prerelease: false
68
+ version_requirements: *70360322205100
69
+ - !ruby/object:Gem::Dependency
70
+ name: guard-rspec
71
+ requirement: &70360322204500 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *70360322204500
80
+ - !ruby/object:Gem::Dependency
81
+ name: growl
82
+ requirement: &70360322203700 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: *70360322203700
91
+ - !ruby/object:Gem::Dependency
92
+ name: rspec
93
+ requirement: &70360322203080 !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ! '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ type: :development
100
+ prerelease: false
101
+ version_requirements: *70360322203080
102
+ - !ruby/object:Gem::Dependency
103
+ name: ruby-debug19
104
+ requirement: &70360322202340 !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ type: :development
111
+ prerelease: false
112
+ version_requirements: *70360322202340
113
+ - !ruby/object:Gem::Dependency
114
+ name: ruby-debug-base19
115
+ requirement: &70360322201460 !ruby/object:Gem::Requirement
116
+ none: false
117
+ requirements:
118
+ - - =
119
+ - !ruby/object:Gem::Version
120
+ version: 0.11.26
121
+ type: :development
122
+ prerelease: false
123
+ version_requirements: *70360322201460
124
+ - !ruby/object:Gem::Dependency
125
+ name: linecache19
126
+ requirement: &70360322200520 !ruby/object:Gem::Requirement
127
+ none: false
128
+ requirements:
129
+ - - =
130
+ - !ruby/object:Gem::Version
131
+ version: 0.5.13
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: *70360322200520
135
+ - !ruby/object:Gem::Dependency
136
+ name: awesome_print
137
+ requirement: &70360322199440 !ruby/object:Gem::Requirement
138
+ none: false
139
+ requirements:
140
+ - - ! '>='
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ type: :development
144
+ prerelease: false
145
+ version_requirements: *70360322199440
14
146
  description: Help create Collection+JSON hypermedia APIs
15
147
  email:
16
148
  - markthedeveloper@gmail.com
@@ -19,17 +151,36 @@ extensions: []
19
151
  extra_rdoc_files: []
20
152
  files:
21
153
  - .gitignore
154
+ - .rspec
22
155
  - Gemfile
156
+ - Guardfile
23
157
  - LICENSE
24
158
  - README.md
25
159
  - Rakefile
26
160
  - collection_json.gemspec
161
+ - doc/hypermedia_client.js.coffee
162
+ - doc/sample.collection.json.js
163
+ - doc/spider-cow.jpg
164
+ - doc/spider-pig.gif
27
165
  - lib/collection_json.rb
28
- - lib/collection_json/crud_operations.rb
29
- - lib/collection_json/query.rb
30
- - lib/collection_json/version.rb
31
- - spec/lib/collection_json_spec.rb
166
+ - lib/collection_json/collection.rb
167
+ - lib/collection_json/decorator.rb
168
+ - lib/collection_json/exceptions.rb
169
+ - lib/collection_json/gem_version.rb
170
+ - lib/collection_json/item.rb
171
+ - lib/collection_json/rack.rb
172
+ - lib/collection_json/rack/parser.rb
173
+ - lib/collection_json/template.rb
174
+ - spec/collection_spec.rb
175
+ - spec/decorator_spec.rb
176
+ - spec/integration_spec.rb
177
+ - spec/item_spec.rb
178
+ - spec/rack/parser_spec.rb
32
179
  - spec/spec_helper.rb
180
+ - spec/support/sample_mvc.rb
181
+ - spec/template_spec.rb
182
+ - temp_gems_for_ruby_debug_with_1.9.3/linecache19-0.5.13.gem
183
+ - temp_gems_for_ruby_debug_with_1.9.3/ruby-debug-base19-0.11.26.gem
33
184
  homepage: https://github.com/markburns/collection_json
34
185
  licenses: []
35
186
  post_install_message:
@@ -50,10 +201,17 @@ required_rubygems_version: !ruby/object:Gem::Requirement
50
201
  version: '0'
51
202
  requirements: []
52
203
  rubyforge_project:
53
- rubygems_version: 1.8.15
204
+ rubygems_version: 1.8.17
54
205
  signing_key:
55
206
  specification_version: 3
56
207
  summary: ! 'As specified by: http://amundsen.com/media-types/collection/format/#objects'
57
208
  test_files:
58
- - spec/lib/collection_json_spec.rb
209
+ - spec/collection_spec.rb
210
+ - spec/decorator_spec.rb
211
+ - spec/integration_spec.rb
212
+ - spec/item_spec.rb
213
+ - spec/rack/parser_spec.rb
59
214
  - spec/spec_helper.rb
215
+ - spec/support/sample_mvc.rb
216
+ - spec/template_spec.rb
217
+ has_rdoc:
@@ -1,4 +0,0 @@
1
- module CollectionJson
2
- module CrudOperations
3
- end
4
- end
@@ -1,3 +0,0 @@
1
- module CollectionJson
2
- VERSION = "0.0.1"
3
- end
@@ -1,97 +0,0 @@
1
- require File.expand_path('spec/spec_helper')
2
-
3
-
4
- class HerdOfSpiderCows
5
- include CollectionJson
6
- extend CollectionJson::CrudOperations
7
- extend CollectionJson::Query
8
-
9
- def initialize items, links=[], queries=[], template={}
10
- @items, @links, @queries, @template = items, links, queries, template
11
- end
12
- end
13
-
14
- #see http://amundsen.com/media-types/collection/format/#link-relations
15
- describe CollectionJson do
16
- let(:spider_cows) { HerdOfSpiderCows.new [1,2,3],
17
- ['/spider_cow/1', '/spider_cow/2', 'spider_cow/3'] }
18
-
19
-
20
- describe "#href" do
21
- specify {spider_cows.href.should == "/herd_of_spider_cows" }
22
-
23
- specify do
24
- spider_cows.href= "/gathering_of_spider_cows"
25
- spider_cows.href.should == "/gathering_of_spider_cows"
26
- end
27
- end
28
-
29
- describe "#links" do
30
- specify "has links" do
31
- spider_cows.links.should ==
32
- ['/spider_cow/1', '/spider_cow/2', 'spider_cow/3']
33
- end
34
-
35
- specify do
36
- spider_cows.links = %w[egg banana cheese]
37
- spider_cows.links.should == %w[egg banana cheese]
38
- end
39
- end
40
-
41
- describe "#collection" do
42
- specify do
43
- spider_cows.collection.should be_a Hash
44
- end
45
-
46
- specify "has items" do
47
- spider_cows.collection[:items].should == [1,2,3]
48
- end
49
-
50
- specify "has links" do
51
- spider_cows.collection[:links].should ==
52
- ['/spider_cow/1', '/spider_cow/2', 'spider_cow/3']
53
- end
54
- end
55
- end
56
-
57
-
58
- describe CollectionJson::CrudOperations do
59
- describe ".create" do
60
- #1.1.2. Adding an Item
61
- #To create a new item in the collection, the client first uses the template object to compose a valid item representation and then uses HTTP POST to send that representation to the server for processing.
62
-
63
- #If the item resource was created successfully, the server responds with a status code of 201 and a Location header that contains the URI of the newly created item resource.
64
- end
65
-
66
-
67
- describe ".read" do
68
- #1.1.3. Reading an Item
69
- #Clients can retrieve an existing item resource by sending an HTTP GET request to the URI of an item resource.
70
- #If the request is valid, the server will respond with a representation of that item resource.
71
- end
72
-
73
- describe ".update" do
74
- #1.1.4. Updating an Item
75
- #To update an existing resource, the client uses the template object as a guide to composing a replacement item representation and then uses HTTP PUT to send that representation to the server.
76
- #If the update is successful, the server will respond with HTTP status code 200 and possibly a representation of the updated item resource representation.
77
-
78
-
79
- end
80
-
81
- describe ".delete" do
82
- #1.1.5. Deleting an Item
83
- #Clients can delete existing resources by sending an HTTP DELETE request to the URI of the item resource.
84
-
85
- #If the delete request is successful, the server SHOULD respond with an HTTP status code of 204.
86
-
87
-
88
- end
89
- end
90
-
91
- #1.2. Query Templates
92
- #Clients that support the Collection+JSON media type SHOULD be able to recognize and parse query templates found within responses. Query templates consist of a data array associated with an href property. The queries array supports query templates.
93
-
94
- #For query templates, the name/value pairs of the data array set are appended to the URI found in the href property associated with the queries array (with a question-mark ["?"] as separator) and this new URI is sent to the processing agent.
95
- describe CollectionJson::Query do
96
-
97
- end