restapi 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +0 -1
- data/Gemfile.lock +0 -10
- data/README.rdoc +18 -12
- data/app/controllers/restapi/restapis_controller.rb +28 -1
- data/app/views/layouts/restapi/restapi.html.erb +1 -0
- data/app/views/restapi/restapis/_params.html.erb +22 -0
- data/app/views/restapi/restapis/_params_plain.html.erb +16 -0
- data/app/views/restapi/restapis/index.html.erb +5 -5
- data/app/views/restapi/restapis/method.html.erb +8 -4
- data/app/views/restapi/restapis/plain.html.erb +70 -0
- data/app/views/restapi/restapis/resource.html.erb +16 -5
- data/app/views/restapi/restapis/static.html.erb +4 -6
- data/lib/restapi.rb +2 -1
- data/lib/restapi/application.rb +72 -22
- data/lib/restapi/client/generator.rb +104 -0
- data/lib/restapi/client/template/Gemfile.tt +5 -0
- data/lib/restapi/client/template/README.tt +3 -0
- data/lib/restapi/client/template/base.rb.tt +33 -0
- data/lib/restapi/client/template/bin.rb.tt +110 -0
- data/lib/restapi/client/template/cli.rb.tt +25 -0
- data/lib/restapi/client/template/cli_command.rb.tt +129 -0
- data/lib/restapi/client/template/client.rb.tt +10 -0
- data/lib/restapi/client/template/resource.rb.tt +17 -0
- data/lib/restapi/dsl_definition.rb +20 -2
- data/lib/restapi/error_description.rb +8 -2
- data/lib/restapi/extractor.rb +143 -0
- data/lib/restapi/extractor/collector.rb +113 -0
- data/lib/restapi/extractor/recorder.rb +122 -0
- data/lib/restapi/extractor/writer.rb +356 -0
- data/lib/restapi/helpers.rb +10 -5
- data/lib/restapi/markup.rb +12 -12
- data/lib/restapi/method_description.rb +52 -8
- data/lib/restapi/param_description.rb +6 -5
- data/lib/restapi/railtie.rb +1 -1
- data/lib/restapi/resource_description.rb +1 -1
- data/lib/restapi/restapi_module.rb +43 -0
- data/lib/restapi/validator.rb +70 -3
- data/lib/restapi/version.rb +1 -1
- data/lib/tasks/restapi.rake +120 -121
- data/restapi.gemspec +0 -2
- data/spec/controllers/restapis_controller_spec.rb +41 -6
- data/spec/controllers/users_controller_spec.rb +51 -12
- data/spec/dummy/app/controllers/application_controller.rb +0 -2
- data/spec/dummy/app/controllers/twitter_example_controller.rb +4 -9
- data/spec/dummy/app/controllers/users_controller.rb +13 -6
- data/spec/dummy/config/initializers/restapi.rb +7 -0
- data/spec/dummy/doc/restapi_examples.yml +28 -0
- metadata +49 -76
- data/app/helpers/restapi/restapis_helper.rb +0 -31
@@ -0,0 +1,104 @@
|
|
1
|
+
#!/bin/env ruby
|
2
|
+
# -*- coding: utf-8 -*-
|
3
|
+
require 'rubygems'
|
4
|
+
require 'thor'
|
5
|
+
require 'thor/group'
|
6
|
+
require 'fileutils'
|
7
|
+
require 'active_support/inflector'
|
8
|
+
|
9
|
+
module Restapi
|
10
|
+
module Client
|
11
|
+
|
12
|
+
class Generator < Thor::Group
|
13
|
+
include Thor::Actions
|
14
|
+
|
15
|
+
# Define arguments and options
|
16
|
+
argument :name
|
17
|
+
|
18
|
+
attr_reader :doc, :resource
|
19
|
+
|
20
|
+
def initialize(*args)
|
21
|
+
super
|
22
|
+
@doc = Restapi.to_json()[:docs]
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.source_root
|
26
|
+
File.expand_path("../template", __FILE__)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.destination_root
|
30
|
+
File.join(FileUtils.pwd, "client")
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.start(client_name)
|
34
|
+
super([client_name.parameterize.underscore], :destination_root => destination_root)
|
35
|
+
end
|
36
|
+
|
37
|
+
def generate_cli
|
38
|
+
template("README.tt", "README")
|
39
|
+
template("Gemfile.tt", "Gemfile")
|
40
|
+
template("bin.rb.tt", "bin/#{name}-client")
|
41
|
+
chmod("bin/#{name}-client", 0755)
|
42
|
+
template("client.rb.tt", "lib/#{name}_client.rb")
|
43
|
+
template("base.rb.tt", "lib/#{name}_client/base.rb")
|
44
|
+
template("cli_command.rb.tt", "lib/#{name}_client/cli_command.rb")
|
45
|
+
doc[:resources].each do |key, resource|
|
46
|
+
@resource = resource
|
47
|
+
template("cli.rb.tt", "lib/#{name}_client/commands/#{resource_name}.thor")
|
48
|
+
template("resource.rb.tt", "lib/#{name}_client/resources/#{resource_name}.rb")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
protected
|
53
|
+
|
54
|
+
def class_base
|
55
|
+
name.camelize
|
56
|
+
end
|
57
|
+
|
58
|
+
def plaintext(text)
|
59
|
+
text.gsub(/<.*?>/, '').gsub("\n",' ').strip
|
60
|
+
end
|
61
|
+
|
62
|
+
# Resource related helper methods:
|
63
|
+
|
64
|
+
def resource_name
|
65
|
+
resource[:name].gsub(/\s/,"_").downcase.singularize
|
66
|
+
end
|
67
|
+
|
68
|
+
def api(method)
|
69
|
+
method[:apis].first
|
70
|
+
end
|
71
|
+
|
72
|
+
def params_in_path(method)
|
73
|
+
api(method)[:api_url].scan(/:([^\/]*)/).map(&:first)
|
74
|
+
end
|
75
|
+
|
76
|
+
def client_args(method)
|
77
|
+
client_args = params_in_path(method).dup
|
78
|
+
client_args << "params = {}" if method[:params].any?
|
79
|
+
client_args
|
80
|
+
end
|
81
|
+
|
82
|
+
def validation_hash(method)
|
83
|
+
if method[:params].any? { |p| p[:params] }
|
84
|
+
method[:params].reduce({}) do |h, p|
|
85
|
+
h.update(p[:name] => (p[:params] ? p[:params].map { |pp| pp[:name] } : nil))
|
86
|
+
end
|
87
|
+
else
|
88
|
+
method[:params].map { |p| p[:name] }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def substituted_url(method)
|
93
|
+
params_in_path(method).reduce(api(method)[:api_url]) { |u, p| u.sub(":#{p}","\#{#{p}}")}
|
94
|
+
end
|
95
|
+
|
96
|
+
def transformation_hash(method)
|
97
|
+
method[:params].find_all { |p| p[:expected_type] == "hash" && !p[:params].nil? }.reduce({}) do |h, p|
|
98
|
+
h.update(p[:name] => p[:params].map { |pp| pp[:name] })
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'rest_client'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module <%= class_base %>Client
|
5
|
+
class Base
|
6
|
+
attr_reader :client
|
7
|
+
|
8
|
+
def initialize(config)
|
9
|
+
@client = RestClient::Resource.new(config[:base_url], :user => config[:username], :password => config[:password])
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(method, path, params = {})
|
13
|
+
ret = client[path].send(method, params)
|
14
|
+
data = JSON.parse(ret.body) rescue ret.body
|
15
|
+
return data, ret
|
16
|
+
end
|
17
|
+
|
18
|
+
def validate_params!(options, valid_keys)
|
19
|
+
return unless options.is_a?(Hash)
|
20
|
+
invalid_keys = options.keys - (valid_keys.is_a?(Hash) ? valid_keys.keys : valid_keys)
|
21
|
+
raise ArgumentError, "Invalid keys: #{invalid_keys.join(", ")}" unless invalid_keys.empty?
|
22
|
+
|
23
|
+
if valid_keys.is_a? Hash
|
24
|
+
valid_keys.each do |key, keys|
|
25
|
+
if options[key]
|
26
|
+
validate_params!(options[key], keys)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require "rubygems" # ruby1.9 doesn't "require" it though
|
3
|
+
require "pathname"
|
4
|
+
require "thor"
|
5
|
+
require 'thor/core_ext/file_binary_read'
|
6
|
+
|
7
|
+
$: << File.expand_path("../../lib", __FILE__)
|
8
|
+
require "<%= name %>_client"
|
9
|
+
require "<%= name %>_client/cli_command"
|
10
|
+
|
11
|
+
module <%= class_base %>Cli
|
12
|
+
class Main < Thor
|
13
|
+
|
14
|
+
def help(meth = nil)
|
15
|
+
if meth && !self.respond_to?(meth)
|
16
|
+
initialize_thorfiles(meth)
|
17
|
+
klass, task = Thor::Util.find_class_and_task_by_namespace(meth)
|
18
|
+
self.class.handle_no_task_error(task, false) if klass.nil?
|
19
|
+
klass.start(["-h", task].compact, :shell => self.shell)
|
20
|
+
else
|
21
|
+
say "<%= name.capitalize %> CLI"
|
22
|
+
say
|
23
|
+
invoke :commands
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
desc "commands [SEARCH]", "List the available commands"
|
28
|
+
def commands(search="")
|
29
|
+
initialize_thorfiles
|
30
|
+
klasses = Thor::Base.subclasses
|
31
|
+
display_klasses(false, false, klasses)
|
32
|
+
end
|
33
|
+
|
34
|
+
class << self
|
35
|
+
private
|
36
|
+
def dispatch(task, given_args, given_options, config)
|
37
|
+
parser = Thor::Options.new({:username => Thor::Option.parse(%w[username -u], :string),
|
38
|
+
:password => Thor::Option.parse(%w[password -p], :string)})
|
39
|
+
opts = parser.parse(given_args)
|
40
|
+
<%= class_base %>Client.client_config[:username] = opts["username"]
|
41
|
+
<%= class_base %>Client.client_config[:password] = opts["password"]
|
42
|
+
#remaining = parser.instance_variable_get("@unknown") # TODO: this is an ugly hack :(
|
43
|
+
remaining = parser.remaining
|
44
|
+
super(task, remaining, given_options, config)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def method_missing(meth, *args)
|
51
|
+
meth = meth.to_s
|
52
|
+
initialize_thorfiles(meth)
|
53
|
+
klass, task = Thor::Util.find_class_and_task_by_namespace(meth)
|
54
|
+
args.unshift(task) if task
|
55
|
+
klass.start(args, :shell => self.shell)
|
56
|
+
end
|
57
|
+
|
58
|
+
# Load the thorfiles. If relevant_to is supplied, looks for specific files
|
59
|
+
# in the thor_root instead of loading them all.
|
60
|
+
#
|
61
|
+
# By default, it also traverses the current path until find Thor files, as
|
62
|
+
# described in thorfiles. This look up can be skipped by suppliying
|
63
|
+
# skip_lookup true.
|
64
|
+
#
|
65
|
+
def initialize_thorfiles(relevant_to=nil, skip_lookup=false)
|
66
|
+
thorfiles.each do |f|
|
67
|
+
Thor::Util.load_thorfile(f, nil, options[:debug])
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def thorfiles
|
72
|
+
Dir[File.expand_path("../../lib/<%= name %>_client/commands/*.thor", __FILE__)]
|
73
|
+
end
|
74
|
+
|
75
|
+
# Display information about the given klasses. If with_module is given,
|
76
|
+
# it shows a table with information extracted from the yaml file.
|
77
|
+
#
|
78
|
+
def display_klasses(with_modules=false, show_internal=false, klasses=Thor::Base.subclasses)
|
79
|
+
klasses -= [Thor, Main, ::<%= class_base %>Client::CliCommand] unless show_internal
|
80
|
+
|
81
|
+
show_modules if with_modules && !thor_yaml.empty?
|
82
|
+
|
83
|
+
list = Hash.new { |h,k| h[k] = [] }
|
84
|
+
groups = []
|
85
|
+
|
86
|
+
# Get classes which inherit from Thor
|
87
|
+
(klasses - groups).each { |k| list[k.namespace.split(":").first] += k.printable_tasks(false) }
|
88
|
+
|
89
|
+
# Get classes which inherit from Thor::Base
|
90
|
+
groups.map! { |k| k.printable_tasks(false).first }
|
91
|
+
list["root"] = groups
|
92
|
+
|
93
|
+
# Order namespaces with default coming first
|
94
|
+
list = list.sort{ |a,b| a[0].sub(/^default/, '') <=> b[0].sub(/^default/, '') }
|
95
|
+
list.each { |n, tasks| display_tasks(n, tasks) unless tasks.empty? }
|
96
|
+
end
|
97
|
+
|
98
|
+
def display_tasks(namespace, list) #:nodoc:
|
99
|
+
say namespace
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
begin
|
106
|
+
<%= class_base %>Cli::Main.start
|
107
|
+
rescue RestClient::Exception => e
|
108
|
+
$stderr.puts e.message
|
109
|
+
exit 1
|
110
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class <%= resource_name.camelize %> < <%= class_base %>Client::CliCommand
|
2
|
+
|
3
|
+
<% resource[:methods].each do |method| -%>
|
4
|
+
desc '<%= method[:name] %>', '<%= api(method)[:short_description] %>'
|
5
|
+
<% params_in_path(method).each do |param| -%>
|
6
|
+
method_option :<%= param %>, :required => 'true'
|
7
|
+
<% end
|
8
|
+
method[:params].map {|p| p[:expected_type] == "hash" ? (p[:params] || p) : p}.flatten.each do |param| -%>
|
9
|
+
method_option :<%= param[:name] %>, :required => <%= param[:required] ? 'true' : 'false' %>, :desc => '<%= plaintext(param[:description]) %>', :type => :<%= param[:expected_type] %>
|
10
|
+
<% end -%>
|
11
|
+
def <%= method[:name] %>
|
12
|
+
<% if params_in_path(method).any? || transformation_hash(method).any?
|
13
|
+
transform_options_params = [params_in_path(method).inspect]
|
14
|
+
transform_options_params << transformation_hash(method).inspect if transformation_hash(method).any? -%>
|
15
|
+
<%= (params_in_path(method) + ["options"]).join(", ") %>, *_ = transform_options(<%= transform_options_params.join(", ").html_safe %>)
|
16
|
+
<% end
|
17
|
+
|
18
|
+
client_args = params_in_path(method).dup
|
19
|
+
client_args << "options" if method[:params].any? -%>
|
20
|
+
data, resp = client.<%= method[:name] %><%= "(#{client_args.join(", ")})" if client_args.any? %>
|
21
|
+
print_data(data)
|
22
|
+
end
|
23
|
+
|
24
|
+
<% end -%>
|
25
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module <%= class_base %>Client
|
2
|
+
class CliCommand < Thor
|
3
|
+
no_tasks do
|
4
|
+
def client
|
5
|
+
resource_class = <%= class_base %>Client::Resources.const_get(self.class.name[/[^:]*$/])
|
6
|
+
@client ||= resource_class.new(<%= class_base %>Client.client_config)
|
7
|
+
end
|
8
|
+
|
9
|
+
def transform_options(inline_params, transform_hash = {})
|
10
|
+
ret = inline_params.map { |p| options[p] }
|
11
|
+
|
12
|
+
# we use not mentioned params without change
|
13
|
+
transformed_options = (options.keys - transform_hash.values.flatten - inline_params).reduce({}) { |h, k| h.update(k => options[k]) }
|
14
|
+
|
15
|
+
transform_hash.each do |sub_key, params|
|
16
|
+
transformed_options[sub_key] = {}
|
17
|
+
params.each { |p| transformed_options[sub_key][p] = options[p] if options.has_key?(p) }
|
18
|
+
end
|
19
|
+
|
20
|
+
ret << transformed_options
|
21
|
+
return *ret
|
22
|
+
end
|
23
|
+
|
24
|
+
def print_data(data)
|
25
|
+
case data
|
26
|
+
when Array
|
27
|
+
print_big_table(table_from_array(data))
|
28
|
+
when Hash
|
29
|
+
print_table(table_from_hash(data))
|
30
|
+
else
|
31
|
+
print_unknown(data)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# unifies the data for further processing. e.g.
|
36
|
+
#
|
37
|
+
# { "user" => {"username" => "test", "password" => "changeme" }
|
38
|
+
#
|
39
|
+
# becomes:
|
40
|
+
#
|
41
|
+
# { "username" => "test", "password" => "changeme" }
|
42
|
+
def normalize_item_data(item)
|
43
|
+
if item.size == 1 && item.values.first.is_a?(Hash)
|
44
|
+
item.values.first
|
45
|
+
else
|
46
|
+
item
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def table_from_array(data)
|
51
|
+
return [] if data.empty?
|
52
|
+
table = []
|
53
|
+
items = data.map { |item| normalize_item_data(item) }
|
54
|
+
columns = items.first.keys
|
55
|
+
table << columns
|
56
|
+
items.each do |item|
|
57
|
+
row = columns.map { |c| item[c] }
|
58
|
+
table << row.map(&:to_s)
|
59
|
+
end
|
60
|
+
return table
|
61
|
+
end
|
62
|
+
|
63
|
+
def table_from_hash(data)
|
64
|
+
return [] if data.empty?
|
65
|
+
table = []
|
66
|
+
normalize_item_data(data).each do |k, v|
|
67
|
+
table << ["#{k}:",v].map(&:to_s)
|
68
|
+
end
|
69
|
+
table
|
70
|
+
end
|
71
|
+
|
72
|
+
def print_unknown(data)
|
73
|
+
say data
|
74
|
+
end
|
75
|
+
|
76
|
+
def print_big_table(table, options={})
|
77
|
+
return if table.empty?
|
78
|
+
|
79
|
+
formats, ident, colwidth = [], options[:ident].to_i, options[:colwidth]
|
80
|
+
options[:truncate] = terminal_width if options[:truncate] == true
|
81
|
+
|
82
|
+
formats << "%-#{colwidth + 2}s" if colwidth
|
83
|
+
start = colwidth ? 1 : 0
|
84
|
+
|
85
|
+
start.upto(table.first.length - 2) do |i|
|
86
|
+
maxima ||= table.max{|a,b| a[i].size <=> b[i].size }[i].size
|
87
|
+
formats << "%-#{maxima + 2}s"
|
88
|
+
end
|
89
|
+
|
90
|
+
formats << "%s"
|
91
|
+
formats[0] = formats[0].insert(0, " " * ident)
|
92
|
+
|
93
|
+
header_printed = false
|
94
|
+
table.each do |row|
|
95
|
+
sentence = ""
|
96
|
+
|
97
|
+
row.each_with_index do |column, i|
|
98
|
+
sentence << formats[i] % column.to_s
|
99
|
+
end
|
100
|
+
|
101
|
+
sentence = truncate(sentence, options[:truncate]) if options[:truncate]
|
102
|
+
$stdout.puts sentence
|
103
|
+
say(set_color("-" * sentence.size, :green)) unless header_printed
|
104
|
+
header_printed = true
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
class << self
|
111
|
+
def help(shell, subcommand = true)
|
112
|
+
list = self.printable_tasks(true, subcommand)
|
113
|
+
Thor::Util.thor_classes_in(self).each do |klass|
|
114
|
+
list += printable_tasks(false)
|
115
|
+
end
|
116
|
+
list.sort!{ |a,b| a[0] <=> b[0] }
|
117
|
+
|
118
|
+
shell.say
|
119
|
+
shell.print_table(list, :indent => 2, :truncate => true)
|
120
|
+
shell.say
|
121
|
+
Thor.send(:class_options_help, shell)
|
122
|
+
end
|
123
|
+
|
124
|
+
def banner(task, namespace = nil, subcommand = false)
|
125
|
+
task.name
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
require '<%= name %>_client/base'
|
2
|
+
|
3
|
+
resource_files = Dir[File.expand_path('../<%= name %>_client/resources/*.rb', __FILE__)]
|
4
|
+
resource_files.each { |f| require f }
|
5
|
+
|
6
|
+
module <%= class_base %>Client
|
7
|
+
def self.client_config
|
8
|
+
@client_config ||= {:base_url => "http://localhost:3000"}
|
9
|
+
end
|
10
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module <%= class_base %>Client
|
2
|
+
module Resources
|
3
|
+
class <%= resource_name.camelize %> < <%= class_base %>Client::Base
|
4
|
+
<% resource[:methods].each do |method| -%>
|
5
|
+
|
6
|
+
def <%= method[:name] %><%= "(#{ client_args(method).join(", ") })" if client_args(method).any? %>
|
7
|
+
<% if method[:params].any? -%>
|
8
|
+
validate_params!(params, <%= validation_hash(method).inspect.html_safe %>)
|
9
|
+
<% end -%>
|
10
|
+
call(:<%= api(method)[:http_method].downcase %>, "<%= substituted_url(method) %>"<%=
|
11
|
+
(api(method)[:http_method].downcase == 'get' ? ", :params => params" : ", params") if method[:params].any? %>)
|
12
|
+
end
|
13
|
+
<% end -%>
|
14
|
+
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|