orthor 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Robotic Foo
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,109 @@
1
+ # Orthor Client
2
+
3
+ The Orthor gem is used to fetch and display your content from orthor.com
4
+
5
+ ## Installation
6
+
7
+ Is easy
8
+
9
+ gem install orthor
10
+
11
+ ## Configuration
12
+
13
+ Add the below to a file and require it:
14
+
15
+ Orthor.setup do
16
+ account "orthor" # your orthor account id
17
+ caching :memory, 600 # cache content in memory for 10 minutes
18
+ end
19
+
20
+ You can specify any Moneta cache store class name, e.g. basic_file, memcache etc. The (optional) second argument is cache expiry in seconds, you can provide any initialize options you want as the 3rd arg, e.g.:
21
+
22
+ Orthor.setup do
23
+ account "bob"
24
+ caching :basic_file, 300, :path => "tmp/"
25
+ end
26
+
27
+ ## Usage
28
+
29
+ The first argument to all these methods is the orthor-id of your piece of content/category etc. The second argument is the name of a template (see below) to use to render the returned content.
30
+
31
+ If you do not provide a template_name to render your content with, you will get the parsed JSON back.
32
+
33
+ ### Content
34
+
35
+ Orthor.content("id", :template_name)
36
+
37
+ ### Queries
38
+
39
+ Orthor.query("id", :template_name)
40
+
41
+ ### Categories
42
+
43
+ Orthor.category("id", :template_name)
44
+
45
+ ### Feeds
46
+
47
+ Orthor.feed("id")
48
+
49
+ ## Templates
50
+
51
+ Orthor returns all of your content in JSON so to make the job of translating JSON -> HTML, we provide some templating help:
52
+
53
+ Orthor::Templates.define do
54
+ template :basic_content, %(<div>{{Content}}</div>)
55
+
56
+ template :blog_entry, %(
57
+ <div class="blog-entry">
58
+ <h2>{{Title}}</h2>
59
+ <div class="blog-content">{{Wysiwyg}}</div>
60
+ </div>)
61
+ template :blog_entry_brief, %(
62
+ <div class="blog-entry">
63
+ <h2><a href="{{URL}}">{{Title}}</a></h2>
64
+ <p>{{Published on}}</p>
65
+ <div class="blog-content">{{Wysiwyg.blurb}}</div>
66
+ </div>)
67
+
68
+ template :user_manual, %(
69
+ <div class="user-manual-entry">
70
+ <h2>{{Title}}</h2>
71
+ <p class="last-updated">Last updated: {{Updated on}}</p>
72
+ <div class="content">{{Wysiwyg}}</div>
73
+ </div>)
74
+ end
75
+
76
+ Now any call you make to Orthor.content/category/query can be given a 2nd argument of a template name and rather than receiving parsed JSON, you will be returned a HTML string.
77
+
78
+ ### Default template tags
79
+
80
+ These tags are available to all content:
81
+
82
+ {{id}} - the orthor id of your content item
83
+ {{Title}} - your content title
84
+ {{Created on}} - the date your content was created
85
+ {{Updated on}} - the date your content was last updated
86
+ {{Author}} - a hash of the original authors details
87
+ {{Updater}} - a hash of the updater details
88
+ {{Published on}} - the date your content was published
89
+ {{URL}} - the URL for your content as defined in Orthor, or auto generated as /:template-prefix/category-id/content-id
90
+ {{template}} - the name of the template this content was created from
91
+ {{category}} - a hash of this items category details
92
+
93
+ Other attributes that will be present on a per item specific basis are are the names of your template elements, e.g.
94
+
95
+ {{Content}}
96
+ {{Featured News Item}}
97
+ {{Supporting Image}}
98
+ {{Markdown body}}
99
+
100
+ ## Questions? Comments?
101
+
102
+ anthony@orthor.com
103
+
104
+ ## Examples
105
+
106
+ [Orthor demo site](http://github.com/anthony/orthor-demo)
107
+
108
+ Copyright (c) 2009 Robotic Foo, released under the MIT license
109
+
@@ -0,0 +1,90 @@
1
+ require 'moneta'
2
+ require 'json'
3
+
4
+ require 'orthor/templates'
5
+
6
+ class Orthor
7
+ class << self
8
+ attr_accessor :cache, :cache_expiry, :account_id
9
+ end
10
+
11
+ def self.content(id, template = nil)
12
+ Templates.render(get(:content_items, id), template)
13
+ end
14
+
15
+ def self.query(id, template = nil)
16
+ Templates.render(get(:queries, id), template)
17
+ end
18
+
19
+ def self.category(id, template = nil)
20
+ Templates.render(get(:categories, id), template)
21
+ end
22
+
23
+ def self.feed(id)
24
+ get(:feeds, id)
25
+ end
26
+
27
+ def self.render(items, template)
28
+ Templates.render(items, template)
29
+ end
30
+
31
+ def self.caching(file, expiry = nil, options = {})
32
+ expiry, options = nil, expiry if expiry.is_a?(Hash)
33
+
34
+ klass = file.to_s.split('_').collect { |e| e.capitalize }.join
35
+ Moneta.autoload(klass.to_sym, "moneta/#{file}")
36
+ self.cache = Moneta.const_get(klass).new(options)
37
+ self.cache_expiry = expiry
38
+ end
39
+
40
+ def self.account(id)
41
+ self.account_id = id
42
+ end
43
+
44
+ def self.setup(&block)
45
+ raise ArgumentError unless block_given?
46
+ self.cache = false # default
47
+
48
+ class_eval &block
49
+ end
50
+
51
+ private
52
+ def self.get(type, id)
53
+ raise "No orthor account given... Please add one to your Orthor setup block" unless account_id
54
+
55
+ begin
56
+ suffix = type == :feeds ? "rss" : "json"
57
+ key = "#{id}-#{type}"
58
+ url = "http://content.orthor.com/#{account_id}/#{type}/#{id}.#{suffix}"
59
+
60
+ resp = cache ? get_cached_content(key, url) : get_response(url)
61
+
62
+ return resp unless suffix == "json"
63
+
64
+ JSON.parse(resp)
65
+ rescue => e
66
+ return ""
67
+ end
68
+ end
69
+
70
+ def self.get_cached_content(id, url)
71
+ content = cache[id]
72
+ if content.nil? || content.empty?
73
+ content = get_response(url)
74
+
75
+ options = {}
76
+ options[:expires_in] = cache_expiry if cache_expiry
77
+ cache.store(id, content, options) unless content.empty?
78
+ end
79
+ content
80
+ end
81
+
82
+ def self.get_response(uri)
83
+ r = Net::HTTP.get_response(URI.parse(uri))
84
+ r.code.to_i == 200 ? r.body : ""
85
+ rescue => e
86
+ ""
87
+ end
88
+
89
+ private_class_method :get, :get_cached_content, :get_response
90
+ end
@@ -0,0 +1,89 @@
1
+ class Orthor
2
+ class Templates
3
+ class Unknown < StandardError; end
4
+
5
+ class << self
6
+ attr_accessor :templates, :mappings
7
+ end
8
+
9
+ def self.define(&blk)
10
+ self.templates, self.mappings = {}, {}
11
+ class_eval &blk
12
+ end
13
+
14
+ def self.template(name, markup)
15
+ self.templates[name.to_s] = markup
16
+ end
17
+
18
+ def self.mapping(name, map)
19
+ self.mappings[name] = map
20
+ end
21
+
22
+ def self.[](key)
23
+ self.templates ||= {}
24
+ self.templates[key.to_s]
25
+ end
26
+
27
+ def self.render(items, template_name_or_mapping)
28
+ return items unless template_name_or_mapping
29
+
30
+ items = [items] unless items.is_a?(Array)
31
+ items.inject("") do |html, item|
32
+ html += parse(item, template_name_or_mapping)
33
+ end
34
+ end
35
+
36
+ private
37
+ def self.parse(item, template_name_or_mapping)
38
+ # ensure we're dealing with an array
39
+ unless item.is_a?(Array)
40
+ if item["type"] == "category"
41
+ item = item["content"]
42
+ else
43
+ item = [item]
44
+ end
45
+ end
46
+
47
+ item.collect do |c|
48
+ markup = template_for(c, template_name_or_mapping)
49
+
50
+ c.inject(markup) do |html, (key, val)|
51
+ if val.is_a?(String) && html.include?("{{#{key}.blurb}}")
52
+ val = "#{val.to_s.gsub(/<\/?[^>]*>/, "")[0..150]}..."
53
+ key = "#{key}.blurb"
54
+
55
+ html = html.gsub("{{#{key}}}", val.to_s)
56
+ elsif val.is_a?(Hash) && html.include?("{{#{key}.")
57
+ # we've got something like Author.Email
58
+ html = val.inject(html) { |h, (k, v)| h = h.gsub("{{#{key}.#{k}}}", v.to_s) }
59
+ else
60
+ html = html.gsub("{{#{key}}}", val.to_s)
61
+ end
62
+ end
63
+ end.join("")
64
+ rescue => e
65
+ raise e if e.is_a?(Orthor::Templates::Unknown)
66
+ return ""
67
+ end
68
+
69
+ def self.template_for(item, template_name_or_mapping)
70
+ if [String, Symbol].include?(template_name_or_mapping.class)
71
+ markup = self[template_name_or_mapping]
72
+
73
+ if !markup && self.mappings[template_name_or_mapping]
74
+ # we've actually got a named mapping
75
+ markup = self[self.mappings[template_name_or_mapping][item["template"]]]
76
+ else
77
+ raise Unknown, "Could not find template #{template_name_or_mapping}" unless markup
78
+ end
79
+ end
80
+ if template_name_or_mapping.is_a?(Hash)
81
+ markup = self[template_name_or_mapping[item["template"]]]
82
+ raise Unknown, "Could not find template mapping for #{item["template"]}" unless markup
83
+ end
84
+ markup
85
+ end
86
+
87
+ private_class_method :parse, :template_for
88
+ end
89
+ end
@@ -0,0 +1,125 @@
1
+ require 'spec_helper'
2
+
3
+ describe Orthor::Templates do
4
+ before(:all) do
5
+ FakeWeb.register_uri(:get, "http://content.orthor.com/orthor/content_items/content.json",
6
+ :body => File.join(SPEC_DIR, 'resources', 'content.json'))
7
+ FakeWeb.register_uri(:get, "http://content.orthor.com/orthor/queries/query.json",
8
+ :body => File.join(SPEC_DIR, 'resources', 'query.json'))
9
+ FakeWeb.register_uri(:get, "http://content.orthor.com/orthor/categories/category.json",
10
+ :body => File.join(SPEC_DIR, 'resources', 'category.json'))
11
+ FakeWeb.register_uri(:get, "http://content.orthor.com/orthor/categories/varied.json",
12
+ :body => File.join(SPEC_DIR, 'resources', 'varied_content.json'))
13
+ FakeWeb.register_uri(:get, "http://content.orthor.com/orthor/feeds/feed.rss",
14
+ :body => File.join(SPEC_DIR, 'resources', 'feed.rss'))
15
+ FakeWeb.register_uri(:get, "http://content.orthor.com/orthor/content_items/missing.json",
16
+ :body => "")
17
+
18
+ Orthor.setup do
19
+ account "orthor"
20
+ end
21
+
22
+ Orthor::Templates.define do
23
+ template :basic, %!<h2>{{Title}}</h2><div class="content">{{Content}}</div>!
24
+ template :brief, %!<h2>{{Title}}</h2>!
25
+ template :blurb, %!<div>{{Content.blurb}}</div>!
26
+ template :user, %!<div>{{Creator.Email}}</div><div>{{Creator.First name}} {{Creator.Last name}}</div>!
27
+
28
+ mapping :listing, { "Text Block" => :brief }
29
+ end
30
+ end
31
+
32
+ describe "with no content returned" do
33
+ it "should return an empty string" do
34
+ Orthor.content("missing").should be_empty
35
+ end
36
+ end
37
+
38
+ describe "defining a set of templates" do
39
+ it "should store the defined templates" do
40
+ Orthor::Templates.templates.length.should == 4
41
+ end
42
+
43
+ it "should return templates by name" do
44
+ Orthor::Templates[:basic].should == %!<h2>{{Title}}</h2><div class="content">{{Content}}</div>!
45
+ end
46
+ end
47
+
48
+ describe "nested attributes" do
49
+ it "should support User.Email, User.First name, User.Last name" do
50
+ Orthor.content("content", :user).should == "<div>demo@orthor.com</div><div>Bob Demo</div>"
51
+ end
52
+ end
53
+
54
+ describe "a content item" do
55
+ it "should display content using the named template" do
56
+ Orthor.content("content", :basic).should == %!<h2>What is Orthor</h2><div class="content"><h2>So what is Orthor really??</h2></div>!
57
+ end
58
+
59
+ it "should respect given template name" do
60
+ Orthor.content("content", :brief).should == %!<h2>What is Orthor</h2>!
61
+ end
62
+
63
+ describe "blurb" do
64
+ it "should truncate some of the text, strip html and add ..." do
65
+ Orthor.content("content", :blurb).should == "<div>So what is Orthor really??...</div>"
66
+ end
67
+ end
68
+ end
69
+
70
+ describe "a content query" do
71
+ it "should render the template for each item" do
72
+ Orthor.query("query", :brief).should == %!<h2>User Manual updated</h2><h2>Account event tracking</h2><h2>Tutorials have been updated</h2>!
73
+ end
74
+ end
75
+
76
+ describe "a category" do
77
+ it "should render the template for each item" do
78
+ Orthor.category("category", :brief).should == %!<h2>User Manual updated</h2><h2>Account event tracking</h2><h2>Tutorials have been updated</h2><h2>Content Queries have landed in Orthor</h2>!
79
+ end
80
+ end
81
+
82
+ describe "rendering given a hash/array" do
83
+ before(:all) do
84
+ @json = Orthor.category("category")
85
+ end
86
+
87
+ it "should be able to render an array of items" do
88
+ Orthor.render(@json["content"], :brief).should == "<h2>User Manual updated</h2><h2>Account event tracking</h2><h2>Tutorials have been updated</h2><h2>Content Queries have landed in Orthor</h2>"
89
+ end
90
+
91
+ it "should be able to render an individual hash" do
92
+ Orthor.render(@json["content"].first, :brief).should == "<h2>User Manual updated</h2>"
93
+ end
94
+ end
95
+
96
+ describe "with a non-existant template" do
97
+ it "should raise an error" do
98
+ lambda { Orthor.category("category", :blah) }.should raise_error(Orthor::Templates::Unknown)
99
+ end
100
+ end
101
+
102
+ describe "template mapping" do
103
+ before(:all) do
104
+ @json = Orthor.category("varied")
105
+ end
106
+
107
+ it "should allow you to pass a hash of orthor template names" do
108
+ Orthor.render(@json["content"], { "Brief News Item" => :basic, "Link" => :brief, "Bookmark" => :blurb }).should == "<h2>User Manual updated</h2><div class=\"content\">The User Manual has recently had a big overhaul with increased documentation for lots of Orthor's great features, including Content Queries, RSS Feeds, Custom CSS and more!</div><div>All accounts now have access to event tracking, use this to gain a snapshot of what everyone in your account has been doing, for example, what were the...</div><h2>Tutorials have been updated</h2><div class=\"content\">We've recently made the tutorials section of the site public, stay tuned for some more language examples + increased detail of the existing languages. </div><h2>Content Queries have landed in Orthor</h2>"
109
+ end
110
+
111
+ describe "with a named map" do
112
+ it "should find the template from the map" do
113
+ Orthor.content("content", :listing).should == "<h2>What is Orthor</h2>"
114
+ end
115
+ end
116
+
117
+ describe "when a mapping is missing" do
118
+ it "should raise an Unknown template exception" do
119
+ lambda do
120
+ Orthor.render(@json["content"], { "Brief News Item" => :basic, "Link" => :brief })
121
+ end.should raise_error(Orthor::Templates::Unknown)
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,130 @@
1
+ require 'spec_helper'
2
+
3
+ describe Orthor do
4
+ before(:all) do
5
+ FakeWeb.register_uri(:get, "http://content.orthor.com/orthor/content_items/content.json",
6
+ :body => File.join(SPEC_DIR, 'resources', 'content.json'))
7
+ FakeWeb.register_uri(:get, "http://content.orthor.com/orthor/queries/query.json",
8
+ :body => File.join(SPEC_DIR, 'resources', 'query.json'))
9
+ FakeWeb.register_uri(:get, "http://content.orthor.com/orthor/categories/category.json",
10
+ :body => File.join(SPEC_DIR, 'resources', 'category.json'))
11
+ FakeWeb.register_uri(:get, "http://content.orthor.com/orthor/feeds/feed.rss",
12
+ :body => File.join(SPEC_DIR, 'resources', 'feed.rss'))
13
+ end
14
+
15
+ describe "setup" do
16
+ before(:all) do
17
+ Orthor.setup do
18
+ account nil
19
+ end
20
+ end
21
+
22
+ it "should raise an error if no account id is present" do
23
+ lambda { Orthor.content("content") }.should raise_error
24
+ end
25
+ end
26
+
27
+ describe "getting" do
28
+ before(:all) do
29
+ Orthor.setup do
30
+ account "orthor"
31
+ end
32
+ end
33
+
34
+ it "should return a Hash for a content item" do
35
+ Orthor.content("content").should be_a(Hash)
36
+ end
37
+
38
+ it "should return an array for a query" do
39
+ Orthor.query("query").should be_an(Array)
40
+ end
41
+
42
+ it "should return a hash for a category" do
43
+ Orthor.category("category").should be_a(Hash)
44
+ end
45
+
46
+ it "should return a string for a feed" do
47
+ Orthor.feed("feed").should be_a(String)
48
+ end
49
+
50
+ it 'should be able to fetch content items' do
51
+ Orthor.content("content").to_s.should include("<h2>So what is Orthor really??</h2>")
52
+ end
53
+
54
+ it 'should be able to fetch queries' do
55
+ Orthor.query("query").to_s.should include("big overhaul with increased documentation for lots of")
56
+ end
57
+
58
+ it 'should be able to fetch categories' do
59
+ Orthor.category("category").to_s.should include("The User Manual has recently had a big overhaul")
60
+ end
61
+ end
62
+
63
+ describe "caching" do
64
+ describe "no config" do
65
+ before(:all) do
66
+ Orthor.setup do
67
+ account "orthor"
68
+ end
69
+ end
70
+
71
+ it 'should not cache content if no config is given' do
72
+ Orthor.cache.should == false
73
+ end
74
+ end
75
+
76
+ describe "with config" do
77
+ describe "memory" do
78
+ before(:all) do
79
+ Orthor.setup do
80
+ account "orthor"
81
+ caching :memory, 600
82
+ end
83
+ end
84
+
85
+ it "should create a moneta cache class" do
86
+ Orthor.cache.should be_a(Moneta::Memory)
87
+ end
88
+
89
+ it "should set a cache expiry" do
90
+ Orthor.cache_expiry.should == 600
91
+ end
92
+
93
+ it 'should add content to the cache after requests' do
94
+ Orthor.cache["content-content_items"].should be_empty
95
+ Orthor.content("content")
96
+ Orthor.cache["content-content_items"].should_not be_empty
97
+
98
+ Orthor.should_not_receive(:cache_content)
99
+ Orthor.content("content")
100
+ end
101
+ end
102
+
103
+ describe "basic file store" do
104
+ before(:all) do
105
+ Orthor.setup do
106
+ account "orthor"
107
+ caching :basic_file, 300, :path => "tmp/"
108
+ end
109
+ end
110
+
111
+ it "should create a moneta basic file cache class" do
112
+ Orthor.cache.should be_a(Moneta::BasicFile)
113
+ end
114
+ end
115
+
116
+ describe "optinal expiry time argument" do
117
+ before(:all) do
118
+ Orthor.setup do
119
+ account "orthor"
120
+ caching :basic_file, :path => "tmp/"
121
+ end
122
+ end
123
+
124
+ it "should default to no cache expiry" do
125
+ Orthor.cache_expiry.should be_nil
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,6 @@
1
+ require 'rubygems'
2
+ require 'spec'
3
+ require 'fake_web'
4
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'orthor')
5
+
6
+ SPEC_DIR = File.dirname(__FILE__) unless defined? SPEC_DIR
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: orthor
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Anthony Langhorne
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-06-10 00:00:00 +10:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ version_requirements: &id001 !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ segments:
26
+ - 0
27
+ version: "0"
28
+ name: moneta
29
+ prerelease: false
30
+ requirement: *id001
31
+ type: :runtime
32
+ - !ruby/object:Gem::Dependency
33
+ version_requirements: &id002 !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ segments:
38
+ - 0
39
+ version: "0"
40
+ name: json
41
+ prerelease: false
42
+ requirement: *id002
43
+ type: :runtime
44
+ description:
45
+ email: anthony@orthor.com
46
+ executables: []
47
+
48
+ extensions: []
49
+
50
+ extra_rdoc_files:
51
+ - LICENSE
52
+ - README.md
53
+ files:
54
+ - lib/orthor.rb
55
+ - lib/orthor/templates.rb
56
+ - LICENSE
57
+ - README.md
58
+ has_rdoc: true
59
+ homepage: http://github.com/anthony/orthor-client
60
+ licenses: []
61
+
62
+ post_install_message:
63
+ rdoc_options:
64
+ - --charset=UTF-8
65
+ require_paths:
66
+ - lib
67
+ required_ruby_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ segments:
72
+ - 0
73
+ version: "0"
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ segments:
79
+ - 0
80
+ version: "0"
81
+ requirements: []
82
+
83
+ rubyforge_project:
84
+ rubygems_version: 1.3.6
85
+ signing_key:
86
+ specification_version: 3
87
+ summary: A gem for displaying content from orthor.com
88
+ test_files:
89
+ - spec/orthor/templates_spec.rb
90
+ - spec/orthor_spec.rb
91
+ - spec/spec_helper.rb