amy 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 84014c35977967c74c5d0cd3dc3f979ab252591e
4
+ data.tar.gz: 891c47801a0a669786da8792f57d9d5a46ea778f
5
+ SHA512:
6
+ metadata.gz: 5cec160b99dde0d735405afde3a379729cd153237afcee2dfa8e813e3ff00c3e0222ad472cda3cfb3a6a57efd5806570ea478a3f041deb498c978756fdeecf3e
7
+ data.tar.gz: d14dc381267e319f47cc016ba76b6936a7fa6ff6b0eec257b139c0c55334aa7c2a557f6f9c36070f2c8d1161e379206e60e1cef7709e0e093154e016a4b20bc6
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Pere Urbon-Bayes
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,50 @@
1
+ # Amy
2
+
3
+ ![image](http://rachstick.files.wordpress.com/2012/04/amy-f.jpg)
4
+
5
+ Amy aims to be a documentation engine, something alike rdoc, but for REST APIs. With the motivation in mind to solve this situation, that has not much of options, specially in the ruby world.
6
+
7
+ With two mode of work, Amy can:
8
+
9
+ * Scrap a list of documentation files.
10
+ * Scrap your source code looking for special comments. ``Currently just works for sinatra``
11
+
12
+ and use this info to compose a nice website with your API docs.
13
+
14
+ At the documentation website you will be able to see the resources aggregated per each url pattern, and within them see each method, in a very REST way. In some cases, for the GET, HEAD and OPTIONS methods you will be able to run them and analise the output.
15
+
16
+ ## I want to use it!
17
+
18
+ First of all, you've to understand this is in very pre alpha status, so everything can change in the near feature. But if you are still a brave man, you can take a look at [examples](examples) directory to see who to run this gem.
19
+
20
+ If you've a nice feature in mind, please submit a pull request, every contribution is wellcome.
21
+
22
+ To create the documentation website you've to run:
23
+
24
+ * ` amy [directory with the definitions] `
25
+
26
+ There is an option to create a ``.amy`` file in your main directory where you'll be able to tune a few options for the engine. Take a look at [.amy](.amy) for an example.
27
+
28
+ ## TODO:
29
+
30
+ Next, and preatty obvious thing, is to write a decent documentation.
31
+ I've in mind also to extend the source code parsing capabilities to include:
32
+ * A better content management skills.
33
+ * Support for more web frameworks and programming languages.
34
+
35
+ and I'm sure many others will popup.
36
+
37
+ ## Contributing to Amy
38
+
39
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
40
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
41
+ * Fork the project.
42
+ * Start a feature/bugfix branch.
43
+ * Commit and push until you are happy with your contribution.
44
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
45
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
46
+
47
+ ## Copyright
48
+
49
+ Copyright (c) 2014 Pere Urbon-Bayes. See LICENSE.txt for further details.
50
+
data/bin/amy ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'amy'
4
+
5
+ if (ARGV.size != 1)
6
+ puts "USAGE: amy.rb [dir]"
7
+ exit(0)
8
+ end
9
+
10
+ parser = Amy::Parser.new
11
+ parser.execute ARGV[0]
12
+ exit(1)
@@ -0,0 +1,23 @@
1
+ require 'erb'
2
+ require 'amy/templates/main'
3
+
4
+ module Amy
5
+ class Generator
6
+
7
+ attr_reader :base_dir
8
+
9
+ def initialize(base_dir="doc/")
10
+ @base_dir = base_dir
11
+ end
12
+
13
+ def do(template, object)
14
+ Dir.mkdir(@base_dir) if (not File.exist?(@base_dir) or not File.directory?( @base_dir ))
15
+ ehtml = ERB.new(IO.read(template))
16
+ output = ehtml.result(object.get_binding)
17
+ File.open("#{@base_dir}#{object.path}", 'w') do |f|
18
+ f.write(output)
19
+ end
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,25 @@
1
+ module Parser
2
+ class Defs
3
+
4
+ attr_accessor :method, :url
5
+
6
+ def initialize
7
+ @props = {}
8
+ @method = ""
9
+ @url = ""
10
+ end
11
+
12
+ def add_prop(key, text)
13
+ @props[key] = text
14
+ end
15
+
16
+ def get_props
17
+ @props
18
+ end
19
+
20
+ def empty?
21
+ @props.empty?
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,118 @@
1
+ require 'amy/parser/tokens'
2
+
3
+ module Parser
4
+ class Lexer
5
+
6
+ def initialize
7
+ @filename = ""
8
+ @file = nil
9
+ @debug = false
10
+ end
11
+
12
+ def set_debug(debug)
13
+ @debug = debug
14
+ end
15
+
16
+ def set_input(file)
17
+ @filename = file
18
+ @file = File.new(file)
19
+ end
20
+
21
+ ##
22
+ # Get's the next token
23
+ #
24
+ def next
25
+ raise IOError.new("Stream is at the end of file.") if eof?
26
+ end_of_token = false
27
+ token = ""
28
+ while not end_of_token
29
+ c = @file.getc
30
+ puts "next c: #{c.inspect} v: #{valid_char?(c)} s: #{single_char?(c)} e: #{is_end_character?(c)}" if @debug
31
+ if eof? then
32
+ end_of_token = true
33
+ elsif (single_char?(c)) then
34
+ if (token.empty?) then
35
+ token = c
36
+ next_token = @file.getc
37
+ if ('#' == token and '#' == next_token) then
38
+ token << next_token
39
+ else
40
+ @file.seek(-1, IO::SEEK_CUR)
41
+ end
42
+ else
43
+ @file.seek(-1, IO::SEEK_CUR)
44
+ end
45
+ end_of_token = true
46
+ elsif (valid_char?(c)) then
47
+ token << c
48
+ elsif is_end_character?(c) then
49
+ move_till_next_token
50
+ end_of_token = (not token.empty?)
51
+ end
52
+ end
53
+ puts "next" if @debug
54
+ build_token(token)
55
+ end
56
+
57
+ ##
58
+ # Set's the lexer back to the begin
59
+ #
60
+ def rewind
61
+ @file.rewind
62
+ end
63
+
64
+ def eof?
65
+ @file.eof?
66
+ end
67
+
68
+ def lineno
69
+ @file.lineno
70
+ end
71
+
72
+ private
73
+
74
+ def move_till_next_token
75
+ there = false
76
+ while not there
77
+ c = @file.getc
78
+ puts "move c:#{c.inspect} v: #{valid_char?(c)} s: #{single_char?(c)}" if @debug
79
+ if eof? then
80
+ there = true
81
+ elsif single_char?(c) or valid_char?(c) then
82
+ @file.seek(-1, IO::SEEK_CUR)
83
+ there = true
84
+ end
85
+ end
86
+ puts "move" if @debug
87
+ end
88
+
89
+ def valid_char?(c)
90
+ not (c =~ /[[:alpha:]|[:digit:]|_|@|<|:|-]/).nil?
91
+ end
92
+
93
+ def single_char?(c)
94
+ not (c =~ /['|"|#|\/|\\|\[|\]|\.|\n]/).nil?
95
+
96
+ end
97
+
98
+ def is_white_space?(c)
99
+ not (c =~ /\s/).nil?
100
+ end
101
+
102
+ def is_end_character?(c)
103
+ eof? or single_char?(c) or !valid_char?(c)
104
+ end
105
+
106
+ def build_token(token)
107
+ type = Tokens::STRING
108
+ if (token.start_with?("##"))
109
+ type = Tokens::COMMENT_TAG
110
+ elsif (token.start_with?("#"))
111
+ type = Tokens::COMMENT
112
+ elsif (token.start_with?("@"))
113
+ type = Tokens::PROPERTY
114
+ end
115
+ Token.new(type, token, @file.lineno)
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,91 @@
1
+ require 'amy/parser/lexer'
2
+ require 'amy/parser/defs'
3
+
4
+ module Parser
5
+ class Parser
6
+
7
+ def initialize
8
+ @lexer = Lexer.new
9
+ end
10
+
11
+ def parse(file)
12
+ @lexer.set_input(file)
13
+ parse_tokens(@lexer)
14
+ end
15
+
16
+ private
17
+
18
+ def parse_tokens(lexer)
19
+ defs = []
20
+ while(not lexer.eof?)
21
+ token = lexer.next
22
+ if token.is_ct? then
23
+ _def = parse_defs(lexer)
24
+ defs << _def unless _def.empty?
25
+ end
26
+ end
27
+ return defs
28
+ end
29
+
30
+ def parse_defs(lexer)
31
+ defs = Defs.new
32
+ return defs if (lexer.eof?)
33
+ finish = false
34
+ prop = ""
35
+ value = ""
36
+ mode = :normal
37
+ while (not lexer.eof? and not finish)
38
+ token = lexer.next
39
+ if :normal == mode and token.is_ct? then
40
+ set_property defs, prop, value
41
+ finish = true
42
+ elsif :content == mode and token.is_p? and "@end" == token.value then
43
+ mode = :normal
44
+ set_property defs, prop, value
45
+ elsif prop.empty? and token.is_p? then
46
+ prop = token.value
47
+ mode = :content if "@content" == prop
48
+ elsif not prop.empty? and token.is_p? then
49
+ set_property defs, prop, value
50
+ prop = token.value
51
+ value = ""
52
+ mode = :content if "@content" == prop
53
+ elsif not prop.empty? and token.is_s? then
54
+ value << " #{token.value}"
55
+ end
56
+ end
57
+ lexer.next
58
+ defs.method = lexer.next.value
59
+ finish = false
60
+ lexer.next
61
+ while(not lexer.eof? and not finish)
62
+ token = lexer.next
63
+ if ["'", '"'].include?(token.value) then
64
+ finish = true
65
+ else
66
+ defs.url << token.value
67
+ end
68
+ end
69
+ return defs
70
+ end
71
+
72
+ def set_property(defs, prop, value)
73
+ if ("@params" == prop) then
74
+ params = {}
75
+ fields = value.split(" ")
76
+ last_key = ""
77
+ fields.each_with_index do |field, i|
78
+ if (i%2 == 0) then
79
+ last_key = field
80
+ else
81
+ params[last_key] = field
82
+ last_key = ""
83
+ end
84
+ end
85
+ defs.add_prop prop, params
86
+ else
87
+ defs.add_prop prop, value.strip
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,39 @@
1
+ module Parser
2
+
3
+ module Tokens
4
+ STRING = 'string'
5
+ COMMENT_TAG = 'comment_tag'
6
+ COMMENT = 'comment'
7
+ PROPERTY = 'property'
8
+ TEXT = 'text'
9
+ end
10
+
11
+ class Token
12
+ attr_accessor :type, :value, :lineno
13
+ def initialize(type, value, lineno)
14
+ @type = type
15
+ @value = value
16
+ @lineno = lineno
17
+ end
18
+
19
+ def is_ct?
20
+ Tokens::COMMENT_TAG == @type
21
+ end
22
+ def is_c?
23
+ Tokens::COMMENT == @type or Tokens::COMMENT_TAG == @type
24
+ end
25
+
26
+ def is_p?
27
+ Tokens::PROPERTY == @type
28
+ end
29
+
30
+ def is_s?
31
+ Tokens::STRING == @type
32
+ end
33
+
34
+ def is_a?(type)
35
+ @type == type
36
+ end
37
+
38
+ end
39
+ end
data/lib/amy/parser.rb ADDED
@@ -0,0 +1,129 @@
1
+ require 'fileutils'
2
+ require 'maruku'
3
+ require 'amy/parser/parser'
4
+
5
+ module Amy
6
+ class Parser
7
+
8
+ OPTIONS_FILE = ".amy"
9
+
10
+ def initialize(base_dir = "doc/")
11
+ @generator = Amy::Generator.new base_dir
12
+ @options = load_options_file
13
+ @mode = @options['mode'] || 'file'
14
+ end
15
+
16
+ require 'pp'
17
+
18
+ def execute(dir)
19
+ specs = load_specs dir
20
+ if (@mode == "code")
21
+ specs['links'] = @options['links']
22
+ specs['base_url'] = @options['base_url']
23
+ specs['api_version'] = @options['api_version']
24
+ end
25
+ compile_json_specs_with dir, specs
26
+ generate_main_page_with specs
27
+ copy_styles_and_js
28
+ true
29
+ end
30
+
31
+ private
32
+
33
+ def compile_json_specs_with(dir, specs)
34
+ specs['resources'].each_pair { |resource, options|
35
+ resource_spec = JSON.parse(IO.read(File.join(File.join(dir, options['dir']),"resource.def")))
36
+ options['config'] = build_resource File.join(dir, options['dir']), resource_spec
37
+ } if (@mode == "file")
38
+ File.open("#{Amy::BASE_DIR}/views/js/data.json", 'w') do |f|
39
+ f.write(specs.to_json)
40
+ end
41
+ end
42
+
43
+ def build_resource(dir, specs)
44
+ resources = Dir.new(dir)
45
+ to_skip = [ '.', '..', 'resource.def' ]
46
+ record = specs['config']
47
+ resources.entries.each do |entry|
48
+ next if to_skip.include?(entry)
49
+ content = IO.read File.join(resources.path, entry)
50
+ method = File.basename(entry, File.extname(entry)).upcase
51
+ doc = Maruku.new(content)
52
+ record[method.downcase]['content'] = doc.to_html
53
+ end
54
+ record
55
+ end
56
+
57
+ def generate_main_page_with(specs)
58
+ main_page = Amy::Model::Main.new
59
+ specs['resources'].each_pair { |resource, options|
60
+ main_page.add_resource( { 'resource' => resource, 'title' => options['title'] } )
61
+ }
62
+ main_page.set_links specs['links'] || []
63
+ main_page.set_version specs['api_version']
64
+ main_page.set_base_url specs['base_url']
65
+ @generator.do("#{Amy::BASE_DIR}/views/main.erb.html", main_page)
66
+ end
67
+
68
+ def load_specs(dir)
69
+ if "file" == @mode then
70
+ JSON.parse(IO.read(File.join(dir,"/specs.def")))
71
+ elsif "code" == @mode then
72
+ { 'resources' => parse_source_code(dir) }
73
+ end
74
+ end
75
+
76
+ def parse_source_code(dir)
77
+ parser = ::Parser::Parser.new
78
+ data = {}
79
+ Dir.foreach(dir) do |file|
80
+ next if [".", ".."].include?(file)
81
+ path = File.join(dir, file)
82
+ if (File.directory?(path)) then
83
+ parse_source_code(path).each_pair do |key, val|
84
+ if data[key].nil? then
85
+ data[key] = val
86
+ else
87
+ data[key].merge!(val)
88
+ end
89
+ end
90
+ else
91
+ defs = parser.parse(path)
92
+ if not defs.empty? then
93
+ defs.each do |_def|
94
+ data[_def.url] = { 'title' => '', 'config' => {} } if data[_def.url].nil?
95
+ record = { 'url' => _def.url, 'title' => _def.get_props["@title"], 'content' => '' }
96
+ if _def.get_props['@params'] then
97
+ record['params'] = []
98
+ _def.get_props['@params'].each_pair do |k,v|
99
+ record['params'] << [k,v]
100
+ end
101
+ end
102
+ record['content'] = _def.get_props['@content'].gsub(/\n/,'<br/>') if _def.get_props['@content']
103
+ if _def.get_props['@description'] then
104
+ data[_def.url]['title'] = _def.get_props['@description']
105
+ end
106
+ data[_def.url]['config'][_def.method] = record
107
+ end
108
+ end
109
+ end
110
+ end
111
+ return data
112
+ end
113
+
114
+ def copy_styles_and_js
115
+ base_dir = @generator.base_dir
116
+ Dir.mkdir("#{base_dir}/js")
117
+ FileUtils.cp("#{Amy::BASE_DIR}/views/js/amy.min.js", "#{base_dir}/js/amy.js")
118
+ FileUtils.cp("#{Amy::BASE_DIR}/views/js/data.json", "#{base_dir}/data.json")
119
+ Dir.mkdir("#{base_dir}/style")
120
+ FileUtils.cp("#{Amy::BASE_DIR}/views/style/style.css", "#{base_dir}/style/style.css")
121
+ end
122
+
123
+ def load_options_file
124
+ return {} unless File.exist?(OPTIONS_FILE)
125
+ YAML::load(File.open(OPTIONS_FILE))
126
+ end
127
+
128
+ end
129
+ end
@@ -0,0 +1,36 @@
1
+ module Amy::Model
2
+
3
+ class Main
4
+
5
+ attr_reader :path
6
+
7
+ def initialize
8
+ @resources = []
9
+ @apiversion = ""
10
+ @base_url = ""
11
+ @links = []
12
+ @path = 'index.html'
13
+ end
14
+
15
+ def add_resource(resource)
16
+ @resources << resource
17
+ end
18
+
19
+ def set_links(links=[])
20
+ @links = links
21
+ end
22
+
23
+ def set_version(version)
24
+ @apiversion = version
25
+ end
26
+
27
+ def set_base_url(base_url)
28
+ @base_url = base_url
29
+ end
30
+
31
+ def get_binding
32
+ binding
33
+ end
34
+
35
+ end
36
+ end
data/lib/amy.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'json'
2
+ require 'yaml'
3
+ require 'amy/parser'
4
+ require 'amy/generate'
5
+
6
+ module Amy
7
+ BASE_DIR = File.expand_path(File.join(File.dirname(__FILE__), '..'))
8
+ end
@@ -0,0 +1,79 @@
1
+ class Tabelle
2
+
3
+ constructor: ->
4
+ return
5
+
6
+ setup: (url) ->
7
+ self = this
8
+ request_config =
9
+ url: url+"?date="+(new Date()).getTime()
10
+ type: "GET"
11
+ dataType: "json"
12
+ success: (data) ->
13
+ $('.resources div').remove()
14
+ i = 0
15
+ for resource,config of data['resources']
16
+ resource = self.add_resource(resource, config, i, data['base_url'])
17
+ resource.toggle()
18
+ i++
19
+ self.add_resource_events(data)
20
+ self.toggle_method_if_necessary()
21
+ error: (e) ->
22
+ $('.resources div').remove()
23
+ $.ajax request_config
24
+ return
25
+
26
+ add_resource: (name, resource, i, base_url="") ->
27
+ resource = new Resource(name, resource, i, base_url)
28
+ resource.build()
29
+ return resource
30
+
31
+ add_resource_events: (data) ->
32
+ handlerIn = () ->
33
+ $(this).css("background", "#f5f5f5")
34
+ $(this).children('div').children().css('color', 'black')
35
+ return
36
+ handlerOut = () ->
37
+ $(this).css("background", "#fff")
38
+ $(this).children('div').children().css('color', '#999')
39
+ return
40
+ $(".resource").hover(handlerIn, handlerOut)
41
+ $('.resource').click ->
42
+ event.stopPropagation()
43
+ $(this).next().fadeToggle()
44
+ return
45
+ return
46
+
47
+ toggle_method_if_necessary: ->
48
+ anchor = window.location.hash.replace('!','')
49
+ anchor = anchor.replace(/\//g,"\\/")
50
+ anchor = anchor.replace(/:/g,'\\:')
51
+ fields = anchor.split('#')
52
+ $("#"+fields[1]+" + div .method."+fields[2]+" .content").fadeToggle()
53
+ $("#"+fields[1]+" + div .method."+fields[2]+" .form").fadeToggle()
54
+ $("#"+fields[1]).next().fadeToggle()
55
+ return
56
+
57
+ @toggleMethods: (i) ->
58
+ event.stopPropagation()
59
+ $("#resource#{i}").fadeToggle()
60
+ return
61
+ @showJSON: ->
62
+ event.stopPropagation()
63
+ url = $("#selector #key").val()
64
+ window.open(url, "_self") if url
65
+ return
66
+
67
+ window.toggleMethods = Tabelle.toggleMethods
68
+ window.showJSON = Tabelle.showJSON
69
+
70
+ $(document).ready ->
71
+ tabelle = new Tabelle
72
+ utils = new Utils
73
+ utils.toggle_by_class 'content'
74
+ url = utils.default_config_url()
75
+ $('#header #selector input').val(url)
76
+ tabelle.setup(url)
77
+ $('#header #explore').click ->
78
+ tabelle.setup($('#header #selector input').val())
79
+ return