├── lib
├── coset
│ ├── collection.rb
│ └── mimeparse.rb
└── coset.rb
├── sample
└── glueball.rb
└── test
├── spec_coset_collection.rb
└── spec_coset.rb
/lib/coset/collection.rb:
--------------------------------------------------------------------------------
1 | require 'coset'
2 |
3 | class Coset::Collection < Coset
4 | def initialize(object=[])
5 | @object = object
6 | end
7 |
8 | def to_a
9 | @object.to_a
10 | end
11 | def list
12 | to_a
13 | end
14 |
15 | def <<(item)
16 | @object << item
17 | end
18 | def create(item)
19 | self << item
20 | end
21 |
22 | def [](index)
23 | @object[map_index(index)]
24 | end
25 | def retrieve(index)
26 | self[index]
27 | end
28 |
29 | def []=(index, value)
30 | @object[map_index(index)] = value
31 | end
32 | def update(index, value)
33 | self[index] = value
34 | end
35 |
36 | def delete(index)
37 | @object.delete map_index(index)
38 | end
39 |
40 | def to_s
41 | @object.to_s
42 | end
43 |
44 | def map_index(index)
45 | index
46 | end
47 |
48 | GET "/" do
49 | res.write list
50 | end
51 |
52 | POST "/" do
53 | create req.body
54 | end
55 |
56 | GET "/{index}" do
57 | res.write retrieve(@index)
58 | end
59 |
60 | PUT "/{index}" do
61 | update(@index, req.body)
62 | end
63 |
64 | DELETE "/{index}" do
65 | delete @index
66 | end
67 | end
68 |
69 | class Coset::Collection::Array < Coset::Collection
70 | def map_index(index)
71 | Integer(index)
72 | end
73 |
74 | def delete(index)
75 | @object.delete_at(map_index(index))
76 | end
77 | end
78 |
79 | class Coset::Collection::Hash < Coset::Collection
80 | def initialize(pkey, object={})
81 | super object
82 | @pkey = pkey
83 | end
84 |
85 | def to_a
86 | @object.values
87 | end
88 |
89 | def <<(item)
90 | update(item.fetch(@pkey), item)
91 | end
92 | end
93 |
94 | class Coset::Collection::Atom < Coset::Collection
95 | def initialize(feed=::Atom::Feed.new)
96 | super feed
97 | end
98 |
99 | def list
100 | @object.to_s
101 | end
102 |
103 | def <<(item)
104 | super(::Atom::Entry.parse(item))
105 | end
106 |
107 | def [](index)
108 | @object.entries.find { |e| e.id == index }
109 | end
110 |
111 | def []=(index, value)
112 | value.id = index
113 | self << value
114 | end
115 |
116 | def delete(index)
117 | @object.entries.delete_if { |e| e.id == index }
118 | end
119 | end
120 |
121 |
--------------------------------------------------------------------------------
/sample/glueball.rb:
--------------------------------------------------------------------------------
1 | require 'atom/service'
2 | require 'atom/collection'
3 |
4 | require 'rack'
5 | require 'coset'
6 |
7 | class Glueball < Coset
8 | def initialize(svc=Atom::Service.new)
9 | @svc = svc
10 | end
11 |
12 | GET "/" do
13 | res.write ''
14 | res.write ''
15 | end
16 |
17 | GET "/service.atomsvc" do
18 | res["Content-Type"] = "application/atomserv+xml"
19 | res.write @svc.to_s
20 | end
21 |
22 |
23 | class NotFound < IndexError; end
24 | map_exception NotFound, 404
25 |
26 | def feed
27 | @svc.workspaces.first.collections.find { |feed| feed.id == @feed } or
28 | raise NotFound
29 | end
30 |
31 | def entry
32 | feed.entries.find { |entry| entry.id == @id } or
33 | raise NotFound
34 | end
35 |
36 |
37 | GET "/{feed}" do
38 | feed.entries.each { |entry|
39 | unless entry.edit_url
40 | link = Atom::Link.new.update "rel" => "edit",
41 | "href" => "#{feed.id}/#{entry.id}"
42 | entry.links << link
43 | end
44 | }
45 |
46 | res["Content-Type"] = "application/atom+xml"
47 | res.write feed.to_s
48 | end
49 |
50 | POST "/{feed}" do
51 | new_entry = Atom::Entry.parse(req.body)
52 |
53 | feed << new_entry
54 | res["Content-Type"] = "application/atom+xml"
55 | res.status = 201
56 | res.write new_entry.to_s
57 | end
58 |
59 | GET "/{feed}/{id}" do
60 | res["Content-Type"] = "application/atom+xml"
61 | res.write entry.to_s
62 | end
63 |
64 | PUT "/{feed}/{id}" do
65 | new_entry = Atom::Entry.parse(req.body)
66 | feed << new_entry
67 | new_entry.id = @id
68 |
69 | res["Content-Type"] = "application/atom+xml"
70 | res.write new_entry.to_s
71 | end
72 |
73 | DELETE "/{feed}/{id}" do
74 | feed.entries.delete_if { |e| e.id == @id }
75 | end
76 | end
77 |
78 |
79 | svc = Atom::Service.new
80 | ws = Atom::Workspace.new
81 | col = Atom::Collection.new("/blogone")
82 |
83 | svc.workspaces << ws
84 |
85 | ws.title = "Glueball server"
86 | ws.collections << col
87 |
88 | col.title = "Blog one"
89 | col.id = "blogone"
90 | col << Atom::Entry.new { |e|
91 | e.id = `uuidgen`.strip
92 | e.title = "An entry"
93 | e.content = "the content"
94 | }
95 |
96 | app = Glueball.new(svc)
97 | # app = Rack::Lint.new(app)
98 | app = Rack::ShowExceptions.new(app)
99 | app = Rack::ShowStatus.new(app)
100 | app = Rack::CommonLogger.new(app)
101 |
102 | Rack::Handler::WEBrick.run app, :Port => 9266
103 |
--------------------------------------------------------------------------------
/test/spec_coset_collection.rb:
--------------------------------------------------------------------------------
1 | require 'test/spec'
2 |
3 | require 'coset'
4 | require 'coset/collection'
5 |
6 | require 'atom/feed' # atom-tools
7 |
8 | context "Coset::Collection" do
9 | specify "should support Arrays" do
10 | c = Coset::Collection::Array.new([1,2,3])
11 |
12 | c.list.should.equal [1,2,3]
13 | c.to_a.should.equal [1,2,3]
14 |
15 | c.create(4)
16 | c.list.should.equal [1,2,3,4]
17 |
18 | c << 5
19 | c.list.should.equal [1,2,3,4,5]
20 |
21 | c.retrieve(1).should.equal 2
22 | c[1].should.equal 2
23 |
24 | c.update(1, 4)
25 | c[1].should.equal 4
26 | c.list.should.equal [1,4,3,4,5]
27 | c[1] = 5
28 | c.list.should.equal [1,5,3,4,5]
29 |
30 | c.delete(1)
31 | c.list.should.equal [1,3,4,5]
32 | end
33 |
34 | specify "should support Hashes" do
35 | c = Coset::Collection::Hash.new("name",
36 | {"Simpson" => {"name" => "Simpson",
37 | "firstname" => "Homer"},
38 | "Newton" => {"name" => "Newton",
39 | "firstname" => "Isaac"},
40 | })
41 |
42 | c.list.should.equal [{"name" => "Simpson", "firstname" => "Homer"},
43 | {"name" => "Newton", "firstname" => "Isaac"}]
44 | c.to_a.should.equal [{"name" => "Simpson", "firstname" => "Homer"},
45 | {"name" => "Newton", "firstname" => "Isaac"}]
46 |
47 | c.create({"name" => "Einstein", "firstname" => "Albert"})
48 | c.list.should.equal [{"name" => "Simpson", "firstname" => "Homer"},
49 | {"name" => "Newton", "firstname" => "Isaac"},
50 | {"name" => "Einstein", "firstname" => "Albert"}]
51 |
52 | c.retrieve("Newton").should.equal({"name" => "Newton", "firstname" => "Isaac"})
53 | c["Newton"].should.equal({"name" => "Newton", "firstname" => "Isaac"})
54 |
55 | c.update("Simpson", {"name" => "Simpson", "firstname" => "Thomas"})
56 | c["Simpson"].should.equal({"name" => "Simpson", "firstname" => "Thomas"})
57 | c.list.should.equal [{"name" => "Simpson", "firstname" => "Thomas"},
58 | {"name" => "Newton", "firstname" => "Isaac"},
59 | {"name" => "Einstein", "firstname" => "Albert"}]
60 |
61 |
62 | c.delete("Simpson")
63 | c.list.should.equal [{"name" => "Newton", "firstname" => "Isaac"},
64 | {"name" => "Einstein", "firstname" => "Albert"}]
65 | end
66 |
67 | specify "should support Atom" do
68 | c = Coset::Collection::Atom.new
69 |
70 | c.list.should.equal "
#{Rack::Utils.escape_html PP.pp(self.class.routes, '')}"
43 | end
44 |
45 | attr_reader :res, :req, :env
46 |
47 | def wants(type, &block)
48 | @wants_content_types << type # keep track of order
49 | @wants[type] = block
50 | end
51 |
52 | def run_wants
53 | # File extensions override Accept:-headers.
54 | if exttype = Rack::File::MIME_TYPES[@EXT.to_s[1..-1]]
55 | @env["HTTP_ACCEPT"] = exttype
56 | end
57 |
58 | # If you accept anything, you'll get the first option.
59 | @env.delete "HTTP_ACCEPT" if @env["HTTP_ACCEPT"] == "*/*"
60 | @env["HTTP_ACCEPT"] ||= @wants_content_types.first
61 |
62 | match = MIMEParse.best_match(@wants_content_types, @env["HTTP_ACCEPT"])
63 |
64 | if match
65 | @wants[match].call
66 | else
67 | res.status = 406 # Not Acceptable
68 | end
69 | end
70 |
71 | class << self
72 | def call(env)
73 | new.call(env)
74 | end
75 |
76 | def inherited(newone)
77 | newone.exceptions = (exceptions || {}).dup
78 | newone.routes = (routes || []).dup
79 | end
80 |
81 | attr_accessor :exceptions
82 | attr_accessor :routes
83 |
84 | def define(desc, &block)
85 | meth = method_name desc
86 | verb, fields, rx = *tokenize(desc)
87 | routes << [rx, verb, fields, meth]
88 | define_method(meth, &block)
89 | end
90 |
91 | def GET(desc, &block) define("GET #{desc}", &block) end
92 | def POST(desc, &block) define("POST #{desc}", &block) end
93 | def PUT(desc, &block) define("PUT #{desc}", &block) end
94 | def DELETE(desc, &block) define("DELETE #{desc}", &block) end
95 |
96 | def map_exception(exception, status, message="")
97 | exceptions[exception] = [status, message]
98 | end
99 |
100 | def tokenize(desc)
101 | verb, path = desc.split(" ", 2)
102 | if verb.nil? || path.nil?
103 | raise ArgumentError, "Invalid description string #{desc}"
104 | end
105 |
106 | verb.upcase!
107 |
108 | fields = []
109 | rx = Regexp.new "\\A" + path.gsub(/\{(.*?)(?::(.*?))?\}/) {
110 | fields << $1
111 |
112 | if $1 == "EXT"
113 | "(\\.\\w+)?"
114 | else
115 | case $2
116 | when "numeric"
117 | "(\d+?)"
118 | when "all"
119 | "(.*?)"
120 | when "", nil
121 | "([^/]*?)"
122 | else
123 | raise ArgumentError, "Invalid qualifier #$2 for #$1"
124 | end
125 | end
126 | } + "\\z"
127 |
128 | [verb, fields, rx]
129 | end
130 |
131 | def method_name(desc)
132 | desc.gsub(/\{(.*?)\}/) { $1.upcase }.delete(" ").gsub(/[^\w-]/, '_')
133 | end
134 | end
135 | end
136 |
--------------------------------------------------------------------------------
/lib/coset/mimeparse.rb:
--------------------------------------------------------------------------------
1 | # ADAPTED FROM http://mimeparse.googlecode.com/svn/trunk/mimeparse.rb
2 | # REVISION 3. MIT-LICENSED, ORIGINALLY BY Brendan Taylor.
3 |
4 | # This version has been stripped down by Christian Neukirchen for
5 | # inclusion with Coset.
6 |
7 |
8 | # mimeparse.rb
9 | #
10 | # This module provides basic functions for handling mime-types. It can
11 | # handle matching mime-types against a list of media-ranges. See section
12 | # 14.1 of the HTTP specification [RFC 2616] for a complete explanation.
13 | #
14 | # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
15 | #
16 | # ---------
17 | #
18 | # This is a port of Joe Gregario's mimeparse.py, which can be found at
19 | #