restapi 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/Gemfile +0 -1
  2. data/Gemfile.lock +0 -10
  3. data/README.rdoc +18 -12
  4. data/app/controllers/restapi/restapis_controller.rb +28 -1
  5. data/app/views/layouts/restapi/restapi.html.erb +1 -0
  6. data/app/views/restapi/restapis/_params.html.erb +22 -0
  7. data/app/views/restapi/restapis/_params_plain.html.erb +16 -0
  8. data/app/views/restapi/restapis/index.html.erb +5 -5
  9. data/app/views/restapi/restapis/method.html.erb +8 -4
  10. data/app/views/restapi/restapis/plain.html.erb +70 -0
  11. data/app/views/restapi/restapis/resource.html.erb +16 -5
  12. data/app/views/restapi/restapis/static.html.erb +4 -6
  13. data/lib/restapi.rb +2 -1
  14. data/lib/restapi/application.rb +72 -22
  15. data/lib/restapi/client/generator.rb +104 -0
  16. data/lib/restapi/client/template/Gemfile.tt +5 -0
  17. data/lib/restapi/client/template/README.tt +3 -0
  18. data/lib/restapi/client/template/base.rb.tt +33 -0
  19. data/lib/restapi/client/template/bin.rb.tt +110 -0
  20. data/lib/restapi/client/template/cli.rb.tt +25 -0
  21. data/lib/restapi/client/template/cli_command.rb.tt +129 -0
  22. data/lib/restapi/client/template/client.rb.tt +10 -0
  23. data/lib/restapi/client/template/resource.rb.tt +17 -0
  24. data/lib/restapi/dsl_definition.rb +20 -2
  25. data/lib/restapi/error_description.rb +8 -2
  26. data/lib/restapi/extractor.rb +143 -0
  27. data/lib/restapi/extractor/collector.rb +113 -0
  28. data/lib/restapi/extractor/recorder.rb +122 -0
  29. data/lib/restapi/extractor/writer.rb +356 -0
  30. data/lib/restapi/helpers.rb +10 -5
  31. data/lib/restapi/markup.rb +12 -12
  32. data/lib/restapi/method_description.rb +52 -8
  33. data/lib/restapi/param_description.rb +6 -5
  34. data/lib/restapi/railtie.rb +1 -1
  35. data/lib/restapi/resource_description.rb +1 -1
  36. data/lib/restapi/restapi_module.rb +43 -0
  37. data/lib/restapi/validator.rb +70 -3
  38. data/lib/restapi/version.rb +1 -1
  39. data/lib/tasks/restapi.rake +120 -121
  40. data/restapi.gemspec +0 -2
  41. data/spec/controllers/restapis_controller_spec.rb +41 -6
  42. data/spec/controllers/users_controller_spec.rb +51 -12
  43. data/spec/dummy/app/controllers/application_controller.rb +0 -2
  44. data/spec/dummy/app/controllers/twitter_example_controller.rb +4 -9
  45. data/spec/dummy/app/controllers/users_controller.rb +13 -6
  46. data/spec/dummy/config/initializers/restapi.rb +7 -0
  47. data/spec/dummy/doc/restapi_examples.yml +28 -0
  48. metadata +49 -76
  49. 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,5 @@
1
+ source :rubygems
2
+
3
+ gem 'thor', '0.15.4'
4
+ gem 'json'
5
+ gem 'rest-client', '1.6.1'
@@ -0,0 +1,3 @@
1
+ This is a readme file for this gem.
2
+
3
+ Let the code be with you!
@@ -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