refactoring API/RSS to use server-side router

This commit is contained in:
Sacha Greif 2013-07-04 12:59:39 +09:00
parent d12c585e0c
commit 572f58ef4f
11 changed files with 86 additions and 480 deletions

1
packages/rss/.npm/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
node_modules

7
packages/rss/.npm/README Normal file
View file

@ -0,0 +1,7 @@
This directory and the files immediately inside it are automatically generated
when you change this package's NPM dependencies. Commit the files in this
directory (npm-shrinkwrap.json, .gitignore, and this README) to source control
so that others run the same versions of sub-dependencies.
You should NOT check in the node_modules directory that Meteor automatically
creates; if you are using git, the .gitignore file tells git to ignore it.

12
packages/rss/.npm/npm-shrinkwrap.json generated Normal file
View file

@ -0,0 +1,12 @@
{
"dependencies": {
"rss": {
"version": "0.0.4",
"dependencies": {
"xml": {
"version": "0.0.7"
}
}
}
}
}

7
packages/rss/package.js Normal file
View file

@ -0,0 +1,7 @@
Package.describe("RSS feed generator");
Npm.depends({rss: '0.0.4'});
Package.on_use(function (api) {
api.add_files('rss.js', 'server');
});

1
packages/rss/rss.js Normal file
View file

@ -0,0 +1 @@
RSS = Npm.require('rss');

View file

@ -1,29 +0,0 @@
// serve up api at the right url
Meteor.serve('api', function(request) {
var posts = [];
var limit = parseInt(request.query['limit']);
limit = limit ? limit : 100;
Posts.find({status: STATUS_APPROVED}, {sort: {submitted: -1}, limit: limit}).forEach(function(post) {
var url = (post.url ? post.url : getPostUrl(post._id));
var properties = {
headline: post.headline,
author: post.author,
date: post.submitted,
url: url,
guid: post._id
};
if(post.body)
properties['body'] = post.body;
if(post.url)
properties['domain'] = getDomain(url);
if(twitterName = getTwitterNameById(post.userId))
properties['twitterName'] = twitterName;
posts.push(properties);
});
return JSON.stringify(posts);
});

View file

@ -1,299 +0,0 @@
// code lifted from: https://github.com/dylang/node-xml
// note: streaming stuff isn't going to work.
//
// (The MIT License)
//
// Copyright (c) 2011 Dylan Greene <dylang@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// 'Software'), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
XML = (function() {
var XML_CHARACTER_MAP = {
'&': '&amp;',
'"': '&quot;',
"'": '&apos;',
'<': '&lt;',
'>': '&gt;'
};
var util = {
xml_safe: function(string) {
return string && string.replace ? string.replace(/([&"<>'])/g, function(str, item) {
return XML_CHARACTER_MAP[item];
})
: string;
}
}
var DEFAULT_INDENT = ' ';
function xml (input, options) {
if (typeof options != 'object') {
options = {
indent: options
};
}
var stream = options.stream ? new Stream() : null,
output = "",
interrupted = false,
indent = !options.indent ? ''
: options.indent === true ? DEFAULT_INDENT
: options.indent,
instant = true;
function delay (func) {
if (!instant) {
func();
} else {
process.nextTick(func);
}
}
function append (interrupt, out) {
if (out !== undefined) {
output += out;
}
if (interrupt && !interrupted) {
stream = stream || new Stream();
interrupted = true;
}
if (interrupt && interrupted) {
var data = output;
delay(function () { stream.emit('data', data) });
output = "";
}
}
function add (value, last) {
format(append, resolve(value, indent, indent ? 1 : 0), last);
}
function end() {
if (stream) {
var data = output;
delay(function () { stream.emit('data', data) });
stream.emit('end');
stream.readable = false;
stream.emit('close');
}
}
// disable delay delayed
delay(function () { instant = false });
if (input && input.forEach) {
input.forEach(function (value, i) {
var last;
if (i + 1 === input.length)
last = end;
add(value, last);
});
} else {
add(input, end);
}
if (stream) {
stream.readable = true;
return stream;
}
return output;
}
function element (/*input, …*/) {
var input = Array.prototype.slice.call(arguments),
self = {
_elem: resolve(input)
};
self.push = function (input) {
if (!this.append) {
throw new Error("not assigned to a parent!");
}
var that = this;
var indent = this._elem.indent;
format(this.append, resolve(
input, indent, this._elem.icount + (indent ? 1 : 0)),
function () { that.append(true) });
};
self.close = function (input) {
if (input !== undefined) {
this.push(input);
}
if (this.end) {
this.end();
}
};
return self;
}
function create_indent(character, count) {
return (new Array(count || 0).join(character || ''))
}
function resolve(data, indent, indent_count) {
indent_count = indent_count || 0;
var indent_spaces = create_indent(indent, indent_count);
var name;
var values = data;
var interrupt = false;
if (typeof data == 'object') {
var keys = Object.keys(data);
name = keys[0];
values = data[name];
if (values._elem) {
values._elem.name = name;
values._elem.icount = indent_count;
values._elem.indent = indent;
values._elem.indents = indent_spaces;
values._elem.interrupt = values;
return values._elem;
}
}
var attributes = [],
content = [];
function get_attributes(obj){
var keys = Object.keys(obj);
keys.forEach(function(key){
attributes.push(attribute(key, obj[key]));
});
}
switch(typeof values) {
case 'object':
if (values === null) break;
if (values._attr) {
get_attributes(values._attr);
}
if (values._cdata) {
content.push('<![CDATA[' + values._cdata + ']]>');
}
if (values.forEach) {
content.push('');
values.forEach(function(value) {
if (typeof value == 'object') {
var _name = Object.keys(value)[0];
if (_name == '_attr') {
get_attributes(value._attr);
} else {
content.push(resolve(
value, indent, indent_count + 1));
}
} else {
//string
content.push(create_indent(
indent, indent_count + 1) + util.xml_safe(value));
}
});
content.push('');
}
break;
default:
//string
content.push(util.xml_safe(values));
}
return {
name: name,
interrupt: interrupt,
attributes: attributes,
content: content,
icount: indent_count,
indents: indent_spaces,
indent: indent
};
}
function format(append, elem, end) {
if (typeof elem != 'object') {
return append(false, elem);
}
var len = elem.interrupt ? 1 : elem.content.length;
function proceed () {
while (elem.content.length) {
var value = elem.content.shift();
if (value === undefined) continue;
if (interrupt(value)) return;
format(append, value);
}
append(false, (len > 1 ? elem.indents : '')
+ (elem.name ? '</' + elem.name + '>' : '')
+ (elem.indent && !end ? '\n' : ''));
if (end) {
end();
}
}
function interrupt(value) {
if (value.interrupt) {
value.interrupt.append = append;
value.interrupt.end = proceed;
value.interrupt = false;
append(true);
return true;
}
return false;
}
append(false, elem.indents
+ (elem.name ? '<' + elem.name : '')
+ (elem.attributes.length ? ' ' + elem.attributes.join(' ') : '')
+ (len ? (elem.name ? '>' : '') : (elem.name ? '/>' : ''))
+ (elem.indent && len > 1 ? '\n' : ''));
if (!len) return append(false, elem.indent ? '\n' : '');
if (!interrupt(elem)) {
proceed();
}
}
function attribute(key, value) {
return key + '=' + '"' + util.xml_safe(value) + '"';
}
xml.Element = element;
return xml;
}());

View file

@ -1,111 +0,0 @@
// code lifted from: https://github.com/dylang/node-rss
//
// (The MIT License)
//
// Copyright (c) 2011 Dylan Greene <dylang@gmail.com>
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// 'Software'), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
RSS = (function() {
function RSS (options, items) {
options = options || {};
this.title = options.title || 'Untitled RSS Feed';
this.description = options.description || '';
this.feed_url = options.feed_url;
this.site_url = options.site_url;
this.image_url = options.image_url;
this.author = options.author;
this.items = items || [];
this.item = function (options) {
options = options || {};
var item = {
title: options.title || 'No title',
description: options.description || '',
url: options.url,
guid: options.guid,
categories: options.categories || [],
author: options.author,
date: options.date
};
this.items.push(item);
return this;
};
this.xml = function(indent) {
return '<?xml version="1.0" encoding="UTF-8"?>\n'
+ XML(generateXML(this), indent);
}
}
function ifTruePush(bool, array, data) {
if (bool) {
array.push(data);
}
}
function generateXML (data){
// todo: xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd"
var channel = [];
channel.push({ title: { _cdata: data.title } });
channel.push({ description: { _cdata: data.description || data.title } });
channel.push({ link: data.site_url || 'http://github.com/dylan/node-rss' });
// image_url set?
if (data.image_url) {
channel.push({ image: [ {url: data.image_url}, {title: data.title}, {link: data.site_url} ] });
}
channel.push({ generator: 'NodeJS RSS Module' });
channel.push({ lastBuildDate: new Date().toGMTString() });
ifTruePush(data.feed_url, channel, { 'atom:link': { _attr: { href: data.feed_url, rel: 'self', type: 'application/rss+xml' } } });
// { updated: new Date().toGMTString() }
data.items.forEach(function(item) {
var item_values = [
{ title: { _cdata: item.title } }
];
ifTruePush(item.description, item_values, { description: { _cdata: item.description } });
ifTruePush(item.url, item_values, { link: item.url });
ifTruePush(item.link || item.guid || item.title, item_values, { guid: [ { _attr: { isPermaLink: !item.guid && !!item.url } }, item.guid || item.url || item.title ] });
ifTruePush(item.author || data.author, item_values, { 'dc:creator': { _cdata: item.author || data.author } });
ifTruePush(item.date, item_values, { pubDate: new Date(item.date).toGMTString() });
channel.push({ item: item_values });
});
return { rss: [
{ _attr: {
'xmlns:dc': 'http://purl.org/dc/elements/1.1/',
'xmlns:content': 'http://purl.org/rss/1.0/modules/content/',
'xmlns:atom': 'http://www.w3.org/2005/Atom',
version: '2.0'
} },
{ channel: channel }
] };
}
return RSS;
}());

View file

@ -1,18 +0,0 @@
// Very simple function to serve a particular function at a particular URL
// The request's path has to be _exactly_ path.
// Sure, we could make it smarter, but I'm guessing this isn't the way things
// will work long term.
Meteor.serve = function(path, fn) {
var connect = (typeof(Npm) == "undefined") ? __meteor_bootstrap__.require("connect") : Npm.require("connect");
__meteor_bootstrap__.app
.use(connect.query()) // <- XXX: we can probably assume accounts did this
.use(function(req, res, next) {
var test = ('/' + path);
if (req.url.substring(0, test.length) !== test)
return next();
// just run fn() and return it to the requester
res.end(fn(req))
});
}

58
server/router.js Normal file
View file

@ -0,0 +1,58 @@
serveAPI = function(limit){
var posts = [];
limit = typeof limit == 'undefined' ? 100 : limit;
console.log(limit)
Posts.find({status: STATUS_APPROVED}, {sort: {submitted: -1}, limit: limit}).forEach(function(post) {
var url = (post.url ? post.url : getPostUrl(post._id));
var properties = {
headline: post.headline,
author: post.author,
date: post.submitted,
url: url,
guid: post._id
};
if(post.body)
properties['body'] = post.body;
if(post.url)
properties['domain'] = getDomain(url);
if(twitterName = getTwitterNameById(post.userId))
properties['twitterName'] = twitterName;
posts.push(properties);
});
return JSON.stringify(posts);
}
Meteor.Router.add({
'/feed.xml': function() {
var feed = new RSS({
title: getSetting('title'),
description: getSetting('tagline'),
feed_url: Meteor.absoluteUrl()+'feed.xml',
site_url: Meteor.absoluteUrl(),
image_url: Meteor.absoluteUrl()+'img/favicon.png',
});
Posts.find({status: STATUS_APPROVED}, {sort: {submitted: -1}}).forEach(function(post) {
feed.item({
title: post.headline,
description: post.body+'</br></br> <a href="'+getPostUrl(post._id)+'">Comments</a>',
author: post.author,
date: post.submitted,
url: (post.url ? post.url : getPostUrl(post._id)),
guid: post._id
});
});
return feed.xml();
},
'/api/': serveAPI,
'/api/:limit': serveAPI
});

View file

@ -1,23 +0,0 @@
// serve up RSS at the right url
Meteor.serve('feed.xml', function() {
var feed = new RSS({
title: getSetting('title'),
description: getSetting('tagline'),
feed_url: Meteor.absoluteUrl()+'feed.xml',
site_url: Meteor.absoluteUrl(),
image_url: Meteor.absoluteUrl()+'img/favicon.png',
});
Posts.find({status: STATUS_APPROVED}, {sort: {submitted: -1}}).forEach(function(post) {
feed.item({
title: post.headline,
description: post.body+'</br></br> <a href="'+getPostUrl(post._id)+'">Comments</a>',
author: post.author,
date: post.submitted,
url: (post.url ? post.url : getPostUrl(post._id)),
guid: post._id
});
});
return feed.xml();
});