diff options
57 files changed, 693 insertions, 401 deletions
diff --git a/app/assets/javascripts/_html-autoload.js b/app/assets/javascripts/_html-autoload.js index d96b80b..ac96b60 100644 --- a/app/assets/javascripts/_html-autoload.js +++ b/app/assets/javascripts/_html-autoload.js @@ -1,55 +1,30 @@ (function($) { - var window = this, - options = {}, - content, - nextUrl, - page = 1, - loading = false; - - $.autopager = function(_options) { - var autopager = this.autopager; + var loading = false; + var opts = null; + $.autopager = function(_opts) { var defaults = { - content: "#content", - nextLink: "a[rel=next]", - onStart: function() {}, - onComplete: function() {} + content: $("#content"), + link: $("link[rel=next]"), + onStart: function() { }, + onComplete: function() { } }; + opts = $.extend({}, defaults, _opts); - options = $.extend({}, defaults, _options); - content = $(options.content); - nextUrl = $(options.nextLink).attr("href"); - $(window).scroll(function() { - if (content.offset().top + content.height() < $(document).scrollTop() + $(window).height()) { - $.autopager.loadNext(); + if ((opts.content.offset().top + opts.content.height()) < ($(document).scrollTop() + $(window).height())) { + if (loading || !opts.link) return; + + opts.onStart(); + loading = true; + $.getJSON(opts.link.attr("href"), function(json, status) { + opts.content.append(json.html); + opts.link.attr("href", json.next_url); + loading = false; + opts.onComplete(); + }); } }); - - return this; - }; - - $.extend($.autopager, { - loadNext: function() { - if (loading || !nextUrl) { - return; - } - - loading = true; - options.onStart(); - $.getJSON(nextUrl, insertContent); - return this; - } - }); - - function insertContent(json, status) { - var nextPage = $(json.html); - - page = page + 1; - nextUrl = json.next; - $(options.nextLink).attr("href", nextUrl); - content.append(nextPage); - options.onComplete(); - loading = false; } -})(jQuery); +})($); + diff --git a/app/assets/javascripts/tweets.js b/app/assets/javascripts/tweets.js index 5a0b955..8428842 100644 --- a/app/assets/javascripts/tweets.js +++ b/app/assets/javascripts/tweets.js @@ -1,15 +1,15 @@ //= require _html-autoload $(function() { - $(".pagination").hide(); - $.autopager({ - content: ".tweets", - nextLink: "a[rel=next]", - onStart: function() { - $(".loading").show(); - }, - onComplete: function() { - $(".loading").hide(); - } - }); + $(".pagination").hide(); + $.autopager({ + content: $(".tweets"), + link: $("link[rel=next]"), + onStart: function() { + // $(".loading").show(); + }, + onComplete: function() { + // $(".loading").hide(); + } + }); }); diff --git a/app/controllers/apidocs_controller.rb b/app/controllers/apidocs_controller.rb new file mode 100644 index 0000000..11c0f34 --- /dev/null +++ b/app/controllers/apidocs_controller.rb @@ -0,0 +1,28 @@ +class ApidocsController < ApplicationController + before_filter :reload_docs + + def index + @resources = Apidoc.resources + end + + def endpoint + @resource = Apidoc.resources[params[:resource].to_sym] + + unless @resource + raise Aclog::Exceptions::DocumentNotFound + end + + @endpoint = @resource.endpoints[params[:name].to_sym] + + unless @endpoint + raise Aclog::Exceptions::DocumentNotFound + end + end + + private + def reload_docs + Apidoc.reload_docs if Rails.env.development? + end +end + + diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 37a7820..61a4bf2 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,57 +1,58 @@ -# -*- coding: utf-8 -*- class ApplicationController < ActionController::Base include Aclog::TwitterOauthEchoAuthentication::ControllerMethods protect_from_forgery before_filter :check_format, :check_session after_filter :xhtml - helper_method :logged_in?, :authorized_to_show_user?, :authorized_to_show_best? + helper_method :current_user, :logged_in?, :allowed_to_see_user?, :allowed_to_see_best? protected - def logged_in?; session[:user_id] && session[:account] end - - def authorized_to_show_user?(user) - @authorized_to_show_user ||= {} - @authorized_to_show_user[user.id] ||= begin - if !user.protected? - true - elsif session[:user_id] == user.id - true - elsif session[:account] && session[:account].following?(user.id) - true - elsif request.headers["X-Verify-Credentials-Authorization"] - # OAuth Echo - user_id = authenticate_with_twitter_oauth_echo rescue false - account = Account.find_by(user_id: user_id) - if account && (account.user_id == user.id || account.following?(user.id)) - true - else - false - end - else - false - end + def current_user + if session[:user_id] + User.find(session[:user_id]) + elsif request.headers["X-Verify-Credentials-Authorization"] + user_id = authenticate_with_twitter_oauth_echo + a = Account.find_by(user_id: user_id) + a.user end + rescue + nil end - def authorized_to_show_best?(user) - authorized_to_show_user?(user) && user.registered? && user.account.active? && (!user.account.private? || user.id == session[:user_id]) + def logged_in? + !!current_user end - def authorize_to_show_user!(user) - authorized_to_show_user?(user) or raise Aclog::Exceptions::UserProtected.new(user) + def allowed_to_see_user?(user) + !user.protected? || + logged_in? && (current_user == user || current_user.following?(user)) end - def authorize_to_show_best!(user) - authorize_to_show_user!(user) - raise Aclog::Exceptions::UserNotRegistered.new(user) unless user.registered? && user.account.active? - raise Aclog::Exceptions::AccountPrivate.new(user) if user.account.private? && user.id != session[:user_id] - true + def allowed_to_see_best?(user) + !user.private? || current_user == user + end + + def require_user(user_id: params[:user_id], screen_name: params[:screen_name], public: false) + begin + user = User.find(id: user_id, screen_name: screen_name) + rescue ActiveRecord::RecordNotFound + raise Aclog::Exceptions::UserNotFound + end + + if !allowed_to_see_user?(user) + raise Aclog::Exceptions::UserProtected, user + end + + if public && !allowed_to_see_best?(user) + raise Aclog::Exceptions::AccountPrivate, user + end + + user end private def check_format - unless request.format == :html || request.format == :json || request.format == :rss + unless request.format == :html || request.format == :json || request.format == :rss || request.format == :atom if params[:format] == nil request.format = :html else diff --git a/app/controllers/errors_controller.rb b/app/controllers/errors_controller.rb index bf35c75..a9a88bd 100644 --- a/app/controllers/errors_controller.rb +++ b/app/controllers/errors_controller.rb @@ -1,4 +1,3 @@ -# -*- encoding: utf-8 -*- class ErrorsController < ApplicationController skip_before_filter :check_format before_filter :force_format diff --git a/app/controllers/tweets_controller.rb b/app/controllers/tweets_controller.rb index 3d74913..b86b55d 100644 --- a/app/controllers/tweets_controller.rb +++ b/app/controllers/tweets_controller.rb @@ -1,115 +1,185 @@ class TweetsController < ApplicationController - def show - @tweet = Tweet.find(params[:id]) - @user = @tweet.user - rescue ActiveRecord::RecordNotFound - raise Aclog::Exceptions::TweetNotFound + param_group :pagination_with_page_number do + optional :count, :integer, "The number of tweets to retrieve. Must be less than or equal to 200, defaults to 20." + optional :page, :integer, "The page number of results to retrieve." end - def lookup - @tweets = Tweet.where(id: params[:id].to_s.split(",").map(&:to_i)) + param_group :pagination_with_ids do + param_group :pagination_with_page_number + optional :since_id, :integer, "Returns results with an ID greater than the specified ID." + optional :max_id, :integer, "Returns results with an ID less than or equal to the specified ID." + end + + param_group :user do + optional :user_id, :integer, "The numerical ID of the user for whom to return results for." + optional :screen_name, :string, "The username of the user for whom to return results for." end def index - user_required begin best - rescue + render :best + rescue Aclog::Exceptions::AccountPrivate timeline - render "timeline" - else - render "best" + render :timeline end end + get "tweets/show" + description "Returns a single Tweet, specified by ID." + requires :id, :integer, "The numerical ID of the desired Tweet." + see "tweets#lookup" + def show + @tweet = Tweet.find(params[:id]) + @user = require_user(user_id: @tweet.user_id) + end + + get "tweets/lookup" + description "Returns Tweets, specified by comma-separated IDs." + requires :id, /^\d+(,\d+)*,?$/, "A comma-separated list of Tweet IDs, up to #{Settings.tweets.count.max} are allowed in a single request." + see "tweets#show" + def lookup + @tweets = Tweet.where(id: params[:id].split(",").map(&:to_i)) + end + + get "tweets/best" + description "Returns the best Tweets of a user, specified by username or user ID." + param_group :user + param_group :pagination_with_page_number def best - user_required - check_public! - @tweets = @user.tweets.list(params, force_page: true).reacted.order_by_reactions + @user = require_user(public: true) + @tweets = paginate_with_page_number(@user.tweets.reacted.order_by_reactions) end + # get "tweets/recent" + # description "Returns the best Tweets in the recent three days of a user, specified by username or user ID." + # param_group :user + # param_group :pagination_with_page_number def recent - user_required - check_public! - @tweets = @user.tweets.list(params, force_page: true).recent.reacted.order_by_reactions + @user = require_user(public: true) + @tweets = paginate_with_page_number(@user.tweets.reacted.recent.order_by_reactions) end + get "tweets/timeline" + description "Returns the newest Tweets of a user, specified by username or user ID." + param_group :user + param_group :pagination_with_ids def timeline - user_required - @tweets = @user.tweets.list(params).reacted.order_by_id + @user = require_user + @tweets = paginate(@user.tweets.reacted.order_by_id) end + get "tweets/discoveries" + description "Returns the Tweets which a user specified by username or user ID discovered." + param_group :user + param_group :pagination_with_ids def discoveries - user_required - @tweets = Tweet.list(params, force_page: true).discovered_by(@user).order_by_id + @user = require_user + @tweets = paginate(Tweet.discovered_by(@user).order_by_id) end + get "tweets/favorites" + description "Returns the Tweets which a user specified by username or user ID favorited." + param_group :user + param_group :pagination_with_ids def favorites - user_required - @tweets = Tweet.list(params, force_page: true).favorited_by(@user).order_by_id + @user = require_user + @tweets = paginate(Tweet.favorited_by(@user).order_by_id) end + get "tweets/retweets" + description "Returns the Tweets which a user specified by username or user ID retweeted." + param_group :user + param_group :pagination_with_ids def retweets - user_required - @tweets = Tweet.list(params, force_page: true).retweeted_by(@user).order_by_id + @user = require_user + @tweets = paginate(Tweet.retweeted_by(@user).order_by_id) end + get "tweets/discovered_by" + description "Returns the Tweets which a user specified by username or user ID retweeted." + param_group :user + optional :source_user_id, :integer, "The numerical ID of the subject user." + optional :source_screen_name, :string, "The username of the subject user." + param_group :pagination_with_ids def discovered_by - user_required - user_b_required - @tweets = @user.tweets.list(params).discovered_by(@user_b).order_by_id + @user = require_user + @source_user = require_user(user_id: params[:source_user_id], screen_name: params[:source_screen_name]) + @tweets = paginate(@user.tweets.discovered_by(@source_user).order_by_id) end + get "tweets/all_best" + param_group :pagination_with_page_number def all_best - @tweets = Tweet.list(params, force_page: true).reacted.order_by_reactions + @tweets = paginate_with_page_number(Tweet.reacted.order_by_reactions) end + get "tweets/all_recent" + param_group :pagination_with_page_number def all_recent - @tweets = Tweet.list(params, force_page: true).recent.reacted.order_by_reactions + @tweets = paginate_with_page_number(Tweet.recent.reacted.order_by_reactions) end + get "tweets/all_timeline" + param_group :pagination_with_ids def all_timeline - @tweets = Tweet.list(params).reacted.order_by_id + @tweets = paginate(Tweet.reacted.order_by_id) end + get "tweets/search" + param_group :pagination_with_ids def search - @tweets = Tweet.list(params, force_page: true).parse_query(params[:q].to_s || "").reacted.not_protected.order_by_id - @tweets = @tweets.recent(7) unless @tweets.to_sql.include?("`tweets`.`id`") + @tweets = paginate(Tweet.recent(7).parse_query(params[:q].to_s || "").reacted.not_protected.order_by_id) end private - def user_required - @user = _require_user(params[:user_id], params[:screen_name]) + def paginate(tweets) + if params[:page] + paginate_with_page_number(tweets) + else + tweets = tweets.limit(params_count).max_id(params[:max_id]).since_id(params[:since_id]) + if tweets.length > 0 + @prev_url = url_for(params.tap {|h| h.delete(:max_id) }.merge(since_id: tweets.first.id)) + @next_url = url_for(params.tap {|h| h.delete(:since_id) }.merge(max_id: tweets.last.id - 1)) + end + tweets + end end - def user_b_required - @user_b = _require_user(params[:user_id_b], params[:screen_name_b]) + def paginate_with_page_number(tweets) + page = [params[:page].to_i, 1].max + @prev_url = page == 1 ? nil : url_for(params.merge(page: page - 1)) + @next_url = url_for(params.merge(page: page + 1)) + tweets.limit(params_count).page(page) end - def check_public! - authorize_to_show_best!(@user) + def params_count + @_count ||= [Settings.tweets.count.max, (params[:count] || Settings.tweets.count.default).to_i].min end def render(*args) - if @tweets && request.xhr? - html = render_to_string(partial: "tweet", collection: @tweets.includes(:user), as: :tweet, formats: :html) - n = @tweets.length > 0 ? - url_for(params[:page] ? - params.merge(page: params[:page].to_i + 1) : - params.merge(max_id: @tweets.last.id - 1)) : - nil - super json: {html: html, next: n} - elsif @tweets && !lookup_context.exists?(params[:action], params[:controller]) && request.format == :json - super("_tweets") + if !request.xhr? && request.format == :json + # JSON API / Atom + begin + super(*args) + rescue ActionView::MissingTemplate + if @tweets + super("_tweets") + elsif @tweet + super("_tweet") + else + raise + end + end else - super(*args) + if @tweets && request.xhr? + super(json: { html: render_to_string(partial: "tweet", collection: @tweets, as: :tweet, formats: :html), + next_url: @next_url, + prev_url: @prev_url }) + else + super(*args) + end end end - - def _require_user(user_id, screen_name) - user = User.get(user_id, screen_name) - raise Aclog::Exceptions::UserProtected.new(user) unless authorized_to_show_user?(user) - user - end end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 580e075..71117e7 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,36 +1,61 @@ class UsersController < ApplicationController + param_group :user do + optional :id, :integer, "The numerical ID of the user for whom to return results for." + optional :screen_name, :string, "The username of the user for whom to return results for." + end + + get "/users/stats" + description "Returns the stats of a user, specified by username or user ID." + param_group :user def stats - user_required - @caption = "Profile" - @user_stats = @user.stats - @user_twitter = @user.account.client.user if request.format == :html + @user = require_public_user end + get "/users/discovered_by" + description "Returns the list of the users who discovored the Tweets of a user, specified by username or user ID." + param_group :user def discovered_by - user_required - authorize_to_show_best!(@user) + @user = require_public_user @result = @user.count_discovered_by.take(Settings.users.count) - @caption = "Discovered By" - render "_user_ranking" + + respond_to do |format| + format.html do + @cached_users = User.find(@result.map {|user_id, count| user_id }).map {|user| [user.id, user] }.to_h + end + + format.json do + render "_users_list" + end + end end + get "/users/discovered_users" + description "Returns the list of the users discovored by a user, specified by username or user ID." + param_group :user def discovered_users - user_required - authorize_to_show_best!(@user) + @user = require_public_user @result = @user.count_discovered_users.take(Settings.users.count) - @caption = "Discovered Users" - render "_user_ranking" + + respond_to do |format| + format.html do + @cached_users = User.find(@result.map {|user_id, count| user_id }).map {|user| [user.id, user] }.to_h + end + + format.json do + render "_users_list" + end + end end + # get "/users/screen_name" def screen_name - user_ids = (params[:id] || params[:user_id]).to_s.split(",").map(&:to_i) - result = User.where(id: user_ids).pluck(:id, :screen_name).map {|id, screen_name| {id: id, screen_name: screen_name} } + user_ids = (params[:id] || params[:ids] || params[:user_id] || params[:user_ids]).split(",").map { |i| i.to_i } + result = User.where(id: user_ids).pluck(:id, :screen_name).map { |id, screen_name| { id: id, screen_name: screen_name } } render json: result end private - def user_required - @user = User.get(params[:id] || params[:user_id], params[:screen_name]) - raise Aclog::Exceptions::UserNotFound unless @user + def require_public_user + require_user(user_id: (params[:id] || params[:user_id]), screen_name: params[:screen_name], public: true) end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index e7cb03c..5700099 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,4 +1,3 @@ -# -*- encoding: utf-8 -*- module ApplicationHelper def format_time(dt) dt.to_time.localtime("+09:00").strftime("%Y-%m-%d %H:%M:%S") diff --git a/app/helpers/tweets_helper.rb b/app/helpers/tweets_helper.rb index 7c4050a..332380b 100644 --- a/app/helpers/tweets_helper.rb +++ b/app/helpers/tweets_helper.rb @@ -6,4 +6,14 @@ module TweetsHelper def favorites_truncated?(tweet) (favorites_truncate_count || Float::INFINITY) < [tweet.favorites_count, tweet.retweets_count].max end + + def link_to_source_text(source) + if /^<a href="(.+?)" rel="nofollow">(.+?)<\/a>/ =~ source + link_to $2, $1 + elsif /^<url:(.+?)(?<!\\):(.+?)?>$/ =~ source + link_to(*[$2, $1.gsub(/(https?)%3A/, "\\1:")].map {|m| m.gsub("\\:", ":") }) + else + h source + end + end end diff --git a/app/models/tweet.rb b/app/models/tweet.rb index 2642d3b..d5ad222 100644 --- a/app/models/tweet.rb +++ b/app/models/tweet.rb @@ -42,20 +42,6 @@ class Tweet < ActiveRecord::Base end end - def self.list(params, options = {}) - count = params[:count].to_i - count = Settings.tweets.count.default unless (1..Settings.tweets.count.max) === count - - if params[:page] || options[:force_page] - page = [params[:page].to_i, 1].max - ret = limit(count).page(page) - else - ret = limit(count).max_id(params[:max_id]).since_id(params[:since_id]) - end - - ret - end - def self.delete_from_id(id) return {} if id.is_a?(Array) && id.size == 0 begin diff --git a/app/models/user.rb b/app/models/user.rb index 80b4a9d..f41c68c 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -9,14 +9,34 @@ class User < ActiveRecord::Base "https://twitter.com/#{self.screen_name}" end - def self.get(id, screen_name) - if id - find(id) rescue raise Aclog::Exceptions::UserNotFound - elsif screen_name - where(screen_name: screen_name).order(updated_at: :desc).first or raise Aclog::Exceptions::UserNotFound - else - Aclog::Exceptions::UserNotFound + def following?(user) + raise Aclog::Exceptions::UserNotRegistered unless registered? + account.following?(user.id) + end + + def private? + !registered? || registered? && account.private? + end + + def self.find(*args) + hash = args.first + return super(*args) unless hash.is_a?(Hash) + + hash.each do |key, value| + next if value.nil? + + if key == :id + return super(value) + else + ret = where(key => value).order(updated_at: :desc).first + if ret + return ret + else + raise ActiveRecord::RecordNotFound, "Couldn't find User with #{key}=#{value}" + end + end end + raise ActiveRecord::RecordNotFound, "Couldn't find User without any parameter" end def self.from_receiver(msg) @@ -117,7 +137,7 @@ class User < ActiveRecord::Base ret[user_id][i] = count end end - ret.map(&:flatten).sort_by {|user_id, favorites_count, retweets_count| -(favorites_count + retweets_count) } + ret.map(&:flatten).sort_by {|user_id, *counts| -counts.sum } end end diff --git a/app/views/about/api.html.haml b/app/views/about/api.html.haml index 1c5778b..441f34d 100644 --- a/app/views/about/api.html.haml +++ b/app/views/about/api.html.haml @@ -2,106 +2,11 @@ - caption :title - sidebar :i %p - **ドキュメントは書き直します** + aclog では JSON 形式の API を提供しています。 %p - すべての API は GET です。 + = link_to "API ドキュメント", api_docs_path %p - 鍵垢のページは OAuth Echo を使います。 - X-Auth-Service-Provider は https://api.twitter.com/1.1/account/verify_credentials.json です -%h4 /api/tweets/show -%dl - %dd ツイート表示 - %dt URL - %dd /api/tweets/show.json - %dt パラメータ - %dd id=(tweet_id) -%h4 /api/tweets/(best|favorited|retweeted|recent) -%dl - %dd ユーザーのベストツイート - %dd 最近3日間のベストツイート - %dd /api/tweets/best.json - %dd /api/tweets/recent.json - %dt パラメータ - %dd screen_name=(screen_name) - %dd user_id=(user_id) - %dd - %strong どちらか必須 - %dd count=(1..100) - %dd 一度に返す上限 - %dd page=(1..) - %dd ページ番号 -%h4 /api/tweets/timeline -%dl - %dd 最新ツイート - %dd /api/tweets/timeline.json - %dt パラメータ - %dd screen_name=(screen_name) - %dd user_id=(user_id) - %dd - %strong どちらか必須 - %dd count=(1..100) - %dd 一度に返す上限 - %dd page=(1..) - %dd ページ番号 - %dd max_id / since_id - %dd Twitter API のものと同じです -%h4 /api/tweets/(discoveries|favorites|retweets) -%dl - %dd ユーザーが反応したツイート - %dt URL - %dd /api/tweets/discoveries.json - %dd /api/tweets/favorites.json - %dd /api/tweets/retweets.json - %dt パラメータ - %dd screen_name=(screen_name) - %dd user_id=(user_id) - %dd - %strong どちらか必須 - %dd count=(1..100) - %dd 一度に返す上限 - %dd page=(1..) - %dd ページ番号 - %dd max_id / since_id - %dd Twitter API のものと同じです -%h4 /api/tweets/discovered_by -%dl - %dd B が反応した A の最新ツイート - %dt URL - %dd /api/tweets/discovered_by.json - %dt パラメータ - %dd screen_name=(screen_name) - %dd user_id=(user_id) - %dd - %strong どちらか必須 - %dd screen_name_b=(screen_name) - %dd user_id_b=(user_id) - %dd - %strong どちらか必須 - %dd count=(1..100) - %dd 一度に返す上限 - %dd page=(1..) - %dd ページ番号 - %dd max_id / since_id - %dd Twitter API のものと同じです -%h4 /api/users/stats -%dl - %dd ユーザーの統計 - %dt URL - %dd /api/users/stats.json - %dt パラメータ - %dd screen_name=(screen_name) - %dd user_id=(user_id) - %dd - %strong どちらか必須 -%h4 /api/users/discovered_by -%h4 /api/users/discovered_users -%dl - %dd ユーザーがふぁぼ・RTしたユーザー・ユーザーをふぁぼ・RTしたユーザーを多い順に50人まで返します - %dt URL - %dd /api/users/discovered_by.json - %dd /api/users/discovered_users.json - %dt パラメータ - %dd screen_name=(screen_name) - %dd user_id=(user_id) - %dd - %strong どちらか必須 + 非公開アカウントのデータを取得するために OAuth Echo を使う場合はこれを使用してください。<br /> + X-Auth-Service-Provider: https://api.twitter.com/1.1/account/verify_credentials.json + + diff --git a/app/views/apidocs/endpoint.html.haml b/app/views/apidocs/endpoint.html.haml new file mode 100644 index 0000000..04a9afd --- /dev/null +++ b/app/views/apidocs/endpoint.html.haml @@ -0,0 +1,22 @@ +- title "API Documentation: " +- caption nil +- sidebar :i +%ul.breadcrumb + %li= link_to "Documentation", api_docs_path + %li.active= @endpoint +%h2.page-header= @endpoint +%div + %div= @endpoint.description + %h3 Parameters + %table.table + %tbody + - @endpoint.parameters.each do |parameter| + %tr + %td + %strong= parameter.name + - if parameter.required? + %small required + - else + %small optional + %td + %p= parameter.description diff --git a/app/views/apidocs/index.html.haml b/app/views/apidocs/index.html.haml new file mode 100644 index 0000000..8a65b8c --- /dev/null +++ b/app/views/apidocs/index.html.haml @@ -0,0 +1,18 @@ +- title "API documentation" +- caption nil +- sidebar :i +%ul.breadcrumb + %li.active Documentation +%h2.page-header Resources +- @resources.each do |name, resource| + %h3= resource.name + %table.table + %thead + %th Resource + %th Description + %tbody + - resource.endpoints.each do |action, endpoint| + %tr + %td= link_to endpoint, api_docs_endpoint_path(name, action) + %td= endpoint.description + diff --git a/app/views/layouts/_base.html.haml b/app/views/layouts/_base.html.haml index 2364b50..16c7c00 100644 --- a/app/views/layouts/_base.html.haml +++ b/app/views/layouts/_base.html.haml @@ -7,6 +7,10 @@ = javascript_include_tag "application" = javascript_include_tag params[:controller] %meta{name: "viewport", content: "width=600px"} + - if @next_url + %link{rel: "next", href: @next_url} + - if @prev_url + %link{rel: "prev", href: @prev_url} %body .navbar.navbar-default.navbar-static-top .container diff --git a/app/views/layouts/tweets.html.haml b/app/views/layouts/tweets.html.haml new file mode 100644 index 0000000..bdf8fa0 --- /dev/null +++ b/app/views/layouts/tweets.html.haml @@ -0,0 +1,7 @@ += render layout: "layouts/base" do + .row + .col-sm-3 + .sidebar= yield :sidebar + .col-sm-9 + %h1= yield :caption + .tweets= yield diff --git a/app/views/shared/sidebar/_users.html.haml b/app/views/shared/sidebar/_users.html.haml index 9c4c1a9..7a36208 100644 --- a/app/views/shared/sidebar/_users.html.haml +++ b/app/views/shared/sidebar/_users.html.haml @@ -18,12 +18,10 @@ = "@#{@user.screen_name} #{t("views.sidebar.user_not_registered")}" .user_nav .list-group - - if authorized_to_show_best?(@user) + - if allowed_to_see_best?(@user) = link_to "best", user_path(@user.screen_name), class: "list-group-item" - = link_to "timeline", user_timeline_path(@user.screen_name), class: "list-group-item" - = link_to "discoveries", user_discoveries_path(@user.screen_name), class: "list-group-item" + = link_to "timeline", user_timeline_path(@user.screen_name), class: "list-group-item" + = link_to "discoveries", user_discoveries_path(@user.screen_name), class: "list-group-item" + - if allowed_to_see_best?(@user) = link_to "discovered by", user_discovered_by_path(@user.screen_name), class: "list-group-item" = link_to "discovered users", user_discovered_users_path(@user.screen_name), class: "list-group-item" - - else - = link_to "timeline", user_path(@user.screen_name), class: "list-group-item" - = link_to "discoveries", user_discoveries_path(@user.screen_name), class: "list-group-item" diff --git a/app/views/tweets/_tweet.html.haml b/app/views/tweets/_tweet.html.haml index 40ca4ca..73e6ab9 100644 --- a/app/views/tweets/_tweet.html.haml +++ b/app/views/tweets/_tweet.html.haml @@ -17,11 +17,7 @@ - if tweet.in_reply_to %span.in_reply_to= link_to "in reply to @" + tweet.in_reply_to.user_screen_name, tweet_path(tweet.in_reply_to.id) .text - - if authorized_to_show_user?(tweet.user) - = simple_format(format_tweet_text(tweet.text)) - - else - %span.quiet - ユーザーは非公開です + = simple_format(format_tweet_text(tweet.text)) .meta.clearfix %span.twitter_bird = link_to image_tag("bird_gray_16.png", alt: "Twitter"), tweet.twitter_url, target: "_blank" @@ -30,7 +26,7 @@ - if favorites_truncated?(tweet) %span.show-full= link_to "show full", tweet_path(tweet.id, full: true) %span.source - = raw "via " + link_to(*tweet.source_link_to_params) + = raw "via " + link_to_source_text(tweet.source) .stats %dl - [["favs", tweet.favorites_count, tweet.favoriters], ["retweets", tweet.retweets_count, tweet.retweeters]].each do |title, count, users| diff --git a/app/views/tweets/_tweets.atom.builder b/app/views/tweets/_tweets.atom.builder new file mode 100644 index 0000000..11dea91 --- /dev/null +++ b/app/views/tweets/_tweets.atom.builder @@ -0,0 +1,19 @@ +atom_feed do |feed| + feed.title yield :title + feed.subtitle yield :caption + feed.updated DateTime.now + + @tweets.each do |tweet| + feed.entry(tweet) do |entry| + entry.title "#{tweet.favorites_count}/#{tweet.retweets_count}: #{CGI.unescapeHTML(strip_tags(format_tweet_text(tweet.text)))}" + entry.updated Time.now.iso8601 + entry.summary "Has been favorited by #{tweet.favorites_count} #{tweet.favorites_count != 1 ? "people" : "person"}, " + + "retweeted by #{tweet.retweets_count} #{tweet.retweets_count != 1 ? "people" : "person"}." + entry.author do |author| + author.name "#{tweet.user.name} (@#{tweet.user.screen_name})" + author.uri tweet.user.twitter_url + end + end + end +end + diff --git a/app/views/tweets/_tweets.html.haml b/app/views/tweets/_tweets.html.haml deleted file mode 100644 index 2889984..0000000 --- a/app/views/tweets/_tweets.html.haml +++ /dev/null @@ -1,8 +0,0 @@ -.tweets= render @tweets.includes(:user) -- if @tweets.count > 0 - .loading= image_tag "loading.gif", alt: "loading...", title: nil - .pagination - - if @tweets.offset_value != nil - = link_to raw("Next ›"), params.merge(page: [params[:page].to_i, 1].max + 1), rel: :next - - else - = link_to raw("Next ›"), params.merge(max_id: @tweets.last.id - 1), rel: :next diff --git a/app/views/tweets/_tweets.json.jbuilder b/app/views/tweets/_tweets.json.jbuilder index f3acf04..c5bcd47 100644 --- a/app/views/tweets/_tweets.json.jbuilder +++ b/app/views/tweets/_tweets.json.jbuilder @@ -1,4 +1 @@ -json.array! @tweets do |json, tweet| - json.partial! "tweet", tweet: tweet -end - +json.array!(@tweets, partial: "tweet", as: :tweet) diff --git a/app/views/tweets/_tweets.rss.builder b/app/views/tweets/_tweets.rss.builder deleted file mode 100644 index 2805ace..0000000 --- a/app/views/tweets/_tweets.rss.builder +++ /dev/null @@ -1,20 +0,0 @@ -xml.instruct! :xml -xml.rss version: "2.0", "xmlns:atom" => "http://www.w3.org/2005/Atom" do - xml.channel do - xml.title title - xml.description caption - xml.link url_for(rss: nil, only_path: false) - xml.__send__(:"atom:link", rel: "self", href: request.url, type: "application/rss+xml") - @tweets.each_with_index do |tweet, i| - xml.item do - xml.title "#{tweet.favorites_count}/#{tweet.retweets_count}: #{CGI.unescapeHTML(strip_tags(format_tweet_text(tweet.text)))}" - xml.description "Has been favorited by #{tweet.favorites_count} #{tweet.favorites_count != 1 ? "people" : "person"}, " + - "retweeted by #{tweet.retweets_count} #{tweet.retweets_count != 1 ? "people" : "person"}." - xml.pubDate tweet.tweeted_at.rfc2822 - xml.link tweet_url(tweet.id) - xml.guid tweet_url(tweet.id) + "#" + tweet.reactions_count.to_s - end - end - end -end - diff --git a/app/views/tweets/all_best.html.haml b/app/views/tweets/all_best.html.haml index 7473321..581e592 100644 --- a/app/views/tweets/all_best.html.haml +++ b/app/views/tweets/all_best.html.haml @@ -1,4 +1,4 @@ - title "Top Tweets" - caption :title - sidebar :i -= render "tweets" += render @tweets diff --git a/app/views/tweets/all_recent.html.haml b/app/views/tweets/all_recent.html.haml index 25f901d..b2ceb40 100644 --- a/app/views/tweets/all_recent.html.haml +++ b/app/views/tweets/all_recent.html.haml @@ -1,4 +1,4 @@ - title "Top Recent Tweets" - caption :title - sidebar :i -= render "tweets" += render @tweets diff --git a/app/views/tweets/all_timeline.atom.builder b/app/views/tweets/all_timeline.atom.builder new file mode 100644 index 0000000..2e93b93 --- /dev/null +++ b/app/views/tweets/all_timeline.atom.builder @@ -0,0 +1,4 @@ +title "Newest" +caption :title +xml << render("tweets") + diff --git a/app/views/tweets/all_timeline.html.haml b/app/views/tweets/all_timeline.html.haml index 67a56d9..3ee3b23 100644 --- a/app/views/tweets/all_timeline.html.haml +++ b/app/views/tweets/all_timeline.html.haml @@ -1,4 +1,4 @@ - title "Newest" - caption :title - sidebar :i -= render "tweets" += render @tweets diff --git a/app/views/tweets/best.html.haml b/app/views/tweets/best.html.haml index ff1d111..6c12ef4 100644 --- a/app/views/tweets/best.html.haml +++ b/app/views/tweets/best.html.haml @@ -1,4 +1,4 @@ - title @user.screen_name + "'s Best Tweets" - caption :title - sidebar :users -= render "tweets" += render @tweets diff --git a/app/views/tweets/discovered_by.atom.builder b/app/views/tweets/discovered_by.atom.builder new file mode 100644 index 0000000..18597db --- /dev/null +++ b/app/views/tweets/discovered_by.atom.builder @@ -0,0 +1,4 @@ +title @user.screen_name + "'s Discovered by " + @source_user.screen_name +caption :title +xml << render("tweets") + diff --git a/app/views/tweets/discovered_by.html.haml b/app/views/tweets/discovered_by.html.haml index db190b6..63b0aeb 100644 --- a/app/views/tweets/discovered_by.html.haml +++ b/app/views/tweets/discovered_by.html.haml @@ -1,4 +1,4 @@ -- title @user.screen_name + "'s Discovered by " + @user_b.screen_name +- title @user.screen_name + "'s Discovered by " + @source_user.screen_name - caption :title - sidebar :users -= render "tweets" += render @tweets diff --git a/app/views/tweets/discoveries.atom.builder b/app/views/tweets/discoveries.atom.builder new file mode 100644 index 0000000..c4ecede --- /dev/null +++ b/app/views/tweets/discoveries.atom.builder @@ -0,0 +1,4 @@ +title @user.screen_name + "'s Discoveries" +caption :title +xml << render("tweets") + diff --git a/app/views/tweets/discoveries.html.haml b/app/views/tweets/discoveries.html.haml index 07c741e..bebf34a 100644 --- a/app/views/tweets/discoveries.html.haml +++ b/app/views/tweets/discoveries.html.haml @@ -1,4 +1,4 @@ - title @user.screen_name + "'s Discoveries" - caption :title - sidebar :users -= render "tweets" += render @tweets diff --git a/app/views/tweets/favorites.html.haml b/app/views/tweets/favorites.html.haml index 07c741e..bebf34a 100644 --- a/app/views/tweets/favorites.html.haml +++ b/app/views/tweets/favorites.html.haml @@ -1,4 +1,4 @@ - title @user.screen_name + "'s Discoveries" - caption :title - sidebar :users -= render "tweets" += render @tweets diff --git a/app/views/tweets/recent.html.haml b/app/views/tweets/recent.html.haml index d9b8d28..46d6d34 100644 --- a/app/views/tweets/recent.html.haml +++ b/app/views/tweets/recent.html.haml @@ -1,4 +1,4 @@ - title @user.screen_name + "'s Recent Best Tweets" - caption :title - sidebar :users -= render "tweets" += render @tweets diff --git a/app/views/tweets/retweets.html.haml b/app/views/tweets/retweets.html.haml index 07c741e..bebf34a 100644 --- a/app/views/tweets/retweets.html.haml +++ b/app/views/tweets/retweets.html.haml @@ -1,4 +1,4 @@ - title @user.screen_name + "'s Discoveries" - caption :title - sidebar :users -= render "tweets" += render @tweets diff --git a/app/views/tweets/search.html.haml b/app/views/tweets/search.html.haml index abf10a2..0763b22 100644 --- a/app/views/tweets/search.html.haml +++ b/app/views/tweets/search.html.haml @@ -6,5 +6,5 @@ = field_set_tag do = text_field_tag :q, params[:q] = submit_tag "search", class: "btn", name: nil -= render "tweets" += render @tweets diff --git a/app/views/tweets/show.html.haml b/app/views/tweets/show.html.haml index e086f17..bd7dc10 100644 --- a/app/views/tweets/show.html.haml +++ b/app/views/tweets/show.html.haml @@ -1,4 +1,4 @@ - title CGI.unescapeHTML(strip_tags(format_tweet_text(@tweet.text))) + " from @" + @user.screen_name - caption @user.screen_name + "'s Tweet" - sidebar :users -.tweets= render @tweet += render @tweet diff --git a/app/views/tweets/timeline.atom.builder b/app/views/tweets/timeline.atom.builder new file mode 100644 index 0000000..ca1ad59 --- /dev/null +++ b/app/views/tweets/timeline.atom.builder @@ -0,0 +1,4 @@ +title @user.screen_name + "'s Newest" +caption :title +xml << render("tweets") + diff --git a/app/views/tweets/timeline.html.haml b/app/views/tweets/timeline.html.haml index 04769c4..c326087 100644 --- a/app/views/tweets/timeline.html.haml +++ b/app/views/tweets/timeline.html.haml @@ -1,4 +1,4 @@ - title @user.screen_name + "'s Newest" - caption :title - sidebar :users -= render "tweets" += render @tweets diff --git a/app/views/users/_user_ranking.html.haml b/app/views/users/_user_ranking.html.haml deleted file mode 100644 index 2d4021e..0000000 --- a/app/views/users/_user_ranking.html.haml +++ /dev/null @@ -1,19 +0,0 @@ -- title "User ranking" -- caption :title -- sidebar :users -.users - %ul.list-inline - - @result.each do |user_id, favorites_count, retweets_count| - - target = User.find(user_id) - %li - .avatar - = link_to user_path(target.screen_name) do - = image_tag target.profile_image_url, alt: target.screen_name, title: target.name - .data - .count - - if params[:action] == "discovered_by" - = link_to favorites_count + retweets_count, user_discovered_by_user_path(@user.screen_name, target.screen_name) - - elsif params[:action] == "discovered_users" - = link_to favorites_count + retweets_count, user_discovered_by_user_path(target.screen_name, @user.screen_name) - - else - - raise Exception diff --git a/app/views/users/_user_ranking.json.jbuilder b/app/views/users/_users_list.json.jbuilder index e544fc6..57bcb32 100644 --- a/app/views/users/_user_ranking.json.jbuilder +++ b/app/views/users/_users_list.json.jbuilder @@ -3,3 +3,4 @@ json.array! @result do |user_id, favorites_count, retweets_count| json.favorites_count favorites_count json.retweets_count retweets_count end + diff --git a/app/views/users/discovered_by.html.haml b/app/views/users/discovered_by.html.haml new file mode 100644 index 0000000..c1986bf --- /dev/null +++ b/app/views/users/discovered_by.html.haml @@ -0,0 +1,14 @@ +- title @user.screen_name + " is Discovered by ..." +- caption :title +- sidebar :users +.users + %ul.list-inline + - @result.each do |user_id, reactions_count| + - user = @cached_users[user_id] + %li + .avatar + = link_to user_path(user.screen_name) do + = image_tag user.profile_image_url, alt: user.screen_name, title: user.name + .data + .count + = link_to reactions_count, user_discovered_by_user_path(@user.screen_name, user.screen_name) diff --git a/app/views/users/discovered_users.html.haml b/app/views/users/discovered_users.html.haml new file mode 100644 index 0000000..3d11f25 --- /dev/null +++ b/app/views/users/discovered_users.html.haml @@ -0,0 +1,14 @@ +- title @user.screen_name + " is Discovering ..." +- caption :title +- sidebar :users +.users + %ul.list-inline + - @result.each do |user_id, reactions_count| + - user = @cached_users[user_id] + %li + .avatar + = link_to user_path(user.screen_name) do + = image_tag user.profile_image_url, alt: user.screen_name, title: user.name + .data + .count + = link_to reactions_count, user_discovered_by_user_path(user.screen_name, @user.screen_name) diff --git a/app/views/users/stats.html.haml b/app/views/users/stats.html.haml index 285da92..9bb08c1 100644 --- a/app/views/users/stats.html.haml +++ b/app/views/users/stats.html.haml @@ -3,31 +3,11 @@ - sidebar :users .avatar = image_tag @user.profile_image_url_original, alt: @user.screen_name, width: 128, height: 128 -%p profile -%dl.dl-horizontal - %dt Username - %dd= @user_twitter.screen_name - %dt Name - %dd= raw @user_twitter.name.gsub(/&(?!(amp;|gt;|lt;|quot;|apos;))/, "&") - %dt tweets - %dd= @user_twitter.statuses_count - %dt Following - %dd= @user_twitter.friends_count - %dt Followers - %dd= @user_twitter.followers_count - %dt Favorites - %dd= @user_twitter.favourites_count - %dt Listed - %dd= @user_twitter.listed_count - %dt Bio - %dd= raw @user_twitter.description.gsub(/&(?!(amp;|gt;|lt;|quot;|apos;))/, "&") - if @user.registered? && @user.account.active? %p records %dl.dl-horizontal - %dt Favorited - %dd= @user.stats.favorited_count - %dt Retweeted - %dd= @user.stats.retweeted_count + %dt Received reactions + %dd= @user.stats.reactions_count %dt Registered %dd= format_time @user.account.created_at %dt Last update diff --git a/config/application.rb b/config/application.rb index 38fa741..e13c899 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,10 +1,10 @@ require File.expand_path('../boot', __FILE__) require 'rails/all' - # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. Bundler.require(:default, Rails.env) +require "./lib/apidoc" module Aclog class Application < Rails::Application diff --git a/config/environments/development.rb b/config/environments/development.rb index 37cf220..eeee0e9 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -10,7 +10,7 @@ Aclog::Application.configure do config.eager_load = false # Show full error reports and disable caching. - config.consider_all_requests_local = false#true + config.consider_all_requests_local = true config.action_controller.perform_caching = false # Don't care if the mailer can't send. diff --git a/config/routes.rb b/config/routes.rb index 838d222..9e761ec 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -18,7 +18,7 @@ Aclog::Application.routes.draw do scope "/i/settings", controller: "settings" do get "/", action: "index", as: "settings" - post "/update", action: "update" + post "/update", action: "update" get "/confirm_deactivation", action: "confirm_deactivation" post "/deactivate", action: "deactivate" end @@ -26,6 +26,10 @@ Aclog::Application.routes.draw do scope "/about", controller: "about" do get "/", action: "about", as: "about" get "/api", action: "api", as: "about_api" + scope "/api/docs", controller: "apidocs" do + get "/", action: "index", as: "api_docs" + get "/:resource/:name", action: "endpoint", as: "api_docs_endpoint" + end end scope "/help", controller: "help" do @@ -36,6 +40,7 @@ Aclog::Application.routes.draw do scope "/:screen_name", controller: "users" do get "/discovered_by", action: "discovered_by", as: "user_discovered_by" get "/discovered_users", action: "discovered_users", as: "user_discovered_users" + get "/stats", action: "stats", as: "user_stats" end # Twitter redirect @@ -43,7 +48,7 @@ Aclog::Application.routes.draw do end # HTML or RSS - scope controller: "tweets", constraints: {format: /(html|rss)/} do + scope controller: "tweets", constraints: {format: /(html|atom)/} do scope "i" do get "/best", action: "all_best", as: "best" get "/recent", action: "all_recent", as: "recent" @@ -53,14 +58,14 @@ Aclog::Application.routes.draw do # TweetController / Tweets scope "/:screen_name" do - get "/", action: "index", as: "user" - get "/best", action: "best", as: "user_best" - get "/recent", action: "recent", as: "user_recent" - get "/timeline", action: "timeline", as: "user_timeline" - get "/discoveries", action: "discoveries", as: "user_discoveries" - get "/favorites", action: "favorites", as: "user_favorites" - get "/retweets", action: "retweets", as: "user_retweets" - get "/discovered_by/:screen_name_b", action: "discovered_by", as: "user_discovered_by_user" + get "/", action: "index", as: "user" + get "/best", action: "best", as: "user_best" + get "/recent", action: "recent", as: "user_recent" + get "/timeline", action: "timeline", as: "user_timeline" + get "/discoveries", action: "discoveries", as: "user_discoveries" + get "/favorites", action: "favorites", as: "user_favorites" + get "/retweets", action: "retweets", as: "user_retweets" + get "/discovered_by/:source_screen_name", action: "discovered_by", as: "user_discovered_by_user" end end end diff --git a/lib/aclog/exceptions.rb b/lib/aclog/exceptions.rb index e9ea212..9b7078f 100644 --- a/lib/aclog/exceptions.rb +++ b/lib/aclog/exceptions.rb @@ -15,5 +15,7 @@ module Aclog class UserNotRegistered < UserError; end class UserProtected < UserError; end class AccountPrivate < UserError; end + + class DocumentNotFound < StandardError; end end end diff --git a/lib/apidoc.rb b/lib/apidoc.rb new file mode 100644 index 0000000..fe64f6b --- /dev/null +++ b/lib/apidoc.rb @@ -0,0 +1,12 @@ +Dir.glob(File.expand_path("../apidoc/**/*.rb", __FILE__)) {|file| require file } + +module Apidoc + extend self + + def resources + @@resources ||= {} + end + + def reload_docs + end +end diff --git a/lib/apidoc/controller_dsl.rb b/lib/apidoc/controller_dsl.rb new file mode 100644 index 0000000..61164ca --- /dev/null +++ b/lib/apidoc/controller_dsl.rb @@ -0,0 +1,45 @@ +module Apidoc + module ControllerDsl + include Resources + include Endpoints + include Parameters + + private + def method_added(method_name) + super(method_name) + + if _apidoc_endpoint_started? + orig_method = self.instance_method(method_name) + current_endpoint = _apidoc_current_endpoint + _apidoc_resource.endpoints[method_name] = _apidoc_current_endpoint + self._apidoc_current_endpoint = nil + + define_method(method_name) do |*args| + current_endpoint.validate!(params) + orig_method.bind(self).call(*args) + end + end + end + + def _apidoc_resource + name = self.name.sub(/Controller$/, "").underscore + Apidoc.resources[name.to_sym] ||= Resource.new(name.titleize) + end + + def _apidoc_current_endpoint + @_apidoc_current_endpoint || raise(DslError, "Endpoint definition is not started.") + end + + def _apidoc_current_endpoint=(value) + @_apidoc_current_endpoint = value + end + + def _apidoc_endpoint_started? + @_apidoc_current_endpoint.present? + end + + def _apidoc_param_groups + @_apidoc_param_groups ||= {} + end + end +end diff --git a/lib/apidoc/controller_dsl/endpoint.rb b/lib/apidoc/controller_dsl/endpoint.rb new file mode 100644 index 0000000..5056920 --- /dev/null +++ b/lib/apidoc/controller_dsl/endpoint.rb @@ -0,0 +1,30 @@ +module Apidoc + module ControllerDsl + module Endpoints + def get(endpoint) + _apidoc_endpoint(:get, endpoint) + end + + def post(endpoint) + _apidoc_endpoint(:post, endpoint) + end + + def _apidoc_endpoint(method, endpoint) + if _apidoc_endpoint_started? + raise DslError, "Previous endpoint #{_apidoc_current_endpoint} definition is not completed." + end + + self._apidoc_current_endpoint = Endpoint.new(method, endpoint) + end + + def description(description) + _apidoc_current_endpoint.description = description + end + + def see(action_name) + _apidoc_current_endpoint.sees << action_name.to_sym + end + end + end +end + diff --git a/lib/apidoc/controller_dsl/parameters.rb b/lib/apidoc/controller_dsl/parameters.rb new file mode 100644 index 0000000..39533b6 --- /dev/null +++ b/lib/apidoc/controller_dsl/parameters.rb @@ -0,0 +1,27 @@ +module Apidoc + module ControllerDsl + module Parameters + def requires(name, validation, description) + _apidoc_current_endpoint.parameters << Parameter.new(name, validation, description, required: true) + end + + def optional(name, validation, description) + _apidoc_current_endpoint.parameters << Parameter.new(name, validation, description, required: false) + end + + def param_group(name, &blk) + if block_given? + _apidoc_param_groups[name] = blk + else + blk = _apidoc_param_groups[name] + if blk + blk.call + else + raise DslError, "Parameters group #{name} is not defined." + end + end + end + end + end +end + diff --git a/lib/apidoc/controller_dsl/resources.rb b/lib/apidoc/controller_dsl/resources.rb new file mode 100644 index 0000000..110b564 --- /dev/null +++ b/lib/apidoc/controller_dsl/resources.rb @@ -0,0 +1,12 @@ +module Apidoc + module ControllerDsl + module Resources + private + def resource_description(description) + _apidoc_resource.description = description + end + end + end +end + + diff --git a/lib/apidoc/endpoint.rb b/lib/apidoc/endpoint.rb new file mode 100644 index 0000000..7d44007 --- /dev/null +++ b/lib/apidoc/endpoint.rb @@ -0,0 +1,24 @@ +module Apidoc + class Endpoint + attr_reader :method, :name, :parameters, :sees + attr_accessor :description + + def initialize(method, name) + @method = method + @name = name + @parameters = [] + @sees = [] + @description = nil + end + + def to_s + "#{method.to_s.upcase} #{name}" + end + + def validate!(params) + parameters.each do |parameter| + parameter.validate!(params) + end + end + end +end diff --git a/lib/apidoc/exceptions.rb b/lib/apidoc/exceptions.rb new file mode 100644 index 0000000..ee4685f --- /dev/null +++ b/lib/apidoc/exceptions.rb @@ -0,0 +1,17 @@ +module Apidoc + class Error < StandardError; end + + class ParameterMissing < Error + def initialize(param) + super("Parameter is missing or the value is empty: #{param}") + end + end + + class ParameterInvalid < Error + def initialize(param) + super("Parameter is invalid: #{param}") + end + end + + class DslError < SyntaxError; end +end diff --git a/lib/apidoc/parameter.rb b/lib/apidoc/parameter.rb new file mode 100644 index 0000000..50bdb75 --- /dev/null +++ b/lib/apidoc/parameter.rb @@ -0,0 +1,40 @@ +module Apidoc + class Parameter + attr_reader :name, :validator, :description, :required + alias required? required + + def initialize(name, validation, description, required: false) + @name = name + @validator = parse_validation(validation) + @description = description + @required = required + end + + def parse_validation(validation) + case validation + when :integer + ->(value) { /^[1-9][0-9]*$/ =~ value } + when :string + ->(value) { true } + when Regexp + ->(value) { validation =~ value } + else + if validation.is_a?(Range) && validation.begin.is_a?(Integer) + ->(value) { /^[1-9][0-9]*$/ =~ value && validation === value.to_i } + else + raise DslError, "Not implemented validation type: #{validation}." + end + end + end + + def validate!(params) + if self.required && params[self.name].blank? + raise ParameterMissing, self.name + end + + if !params[self.name].nil? && !self.validator.call(params[self.name]) + raise ParameterInvalid, self.name + end + end + end +end diff --git a/lib/apidoc/railtie.rb b/lib/apidoc/railtie.rb new file mode 100644 index 0000000..9e992a3 --- /dev/null +++ b/lib/apidoc/railtie.rb @@ -0,0 +1,9 @@ +module Apidoc + class Railtie < Rails::Railtie + initializer "apidoc.controller_injections" do + ActiveSupport.on_load :action_controller do + extend ControllerDsl + end + end + end +end diff --git a/lib/apidoc/resource.rb b/lib/apidoc/resource.rb new file mode 100644 index 0000000..b9afdf3 --- /dev/null +++ b/lib/apidoc/resource.rb @@ -0,0 +1,12 @@ +module Apidoc + class Resource + attr_reader :endpoints, :name + attr_accessor :description + + def initialize(name) + @name = name + @description = nil + @endpoints = {} + end + end +end |