yard-api 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ab2e2a361011adda668edbcad1b71ba857fe61ab
4
- data.tar.gz: c3ed5abc5b2de588e9722179e97acbc4c044986a
3
+ metadata.gz: 4174d158765fdfb8f72c2625f74d77e6bff1cf27
4
+ data.tar.gz: e50d4045d98142419848d2898b6a4fcf0989d9dc
5
5
  SHA512:
6
- metadata.gz: d81dc23669668ed42dfc44e15c317b6199ab4be26cecfbc690abc0ab58b1209c0b15a984dd302ca7f0de8c00153f4e102ee0c060a354241724fc054c7b270957
7
- data.tar.gz: 936a31ac15cecde12085d147565e92f51edef9251370ef33d9488ba9aa25d82473d64f48ca756a1235a3e6dbbd3d3f56abcd03ce27079379164e689eaafda338
6
+ metadata.gz: da95663074496a8b87e5ffaa29e922b95a6d2a75f04fd64165bd9f635ea79333777f196d8ecc8f0d6725b32603b27f0bf138a518e6bf6700afec4075e6fd99db
7
+ data.tar.gz: b6672a201e37c8f16829dd9f19a6836c956fb530799dbcab0cdd335a730008732a7f3d0bc8665c9bd2162f9ee991d8ee8c97946125dd977a962a1c8ddd8933c8
data/README.md CHANGED
@@ -40,6 +40,10 @@ Read that file to view all the available options.
40
40
 
41
41
  ## Changelog
42
42
 
43
+ **29/7/2015 [0.3.0]**
44
+
45
+ - major rework of the linking logic, much improvements but some stuff is broken now
46
+
43
47
  **28/7/2015 [0.2.3]**
44
48
 
45
49
  - dropped the `@argument_scope` tag
data/config/yard_api.yml CHANGED
@@ -61,7 +61,7 @@ strict: false
61
61
  #
62
62
  # Just specify the url to your repository and the branch the documentation has
63
63
  # been generated for and things "should work".
64
- #
64
+ #
65
65
  # Example: https://github.com/amireh/yard-api
66
66
  github_url:
67
67
  github_branch: master
@@ -0,0 +1,19 @@
1
+ require 'yard/code_objects/class_object'
2
+
3
+ module YARD::CodeObjects
4
+ class APIObject < Base
5
+ def path
6
+ super().gsub(/\s/, '')
7
+ end
8
+
9
+ def title
10
+ [ object.title, name ].join('::')
11
+ end
12
+ end
13
+
14
+ class ClassObject < NamespaceObject
15
+ def title
16
+ self[:api_id] || super
17
+ end
18
+ end
19
+ end
@@ -4,6 +4,8 @@ module YARD::APIPlugin
4
4
  default_attr :format, 'html'
5
5
  default_attr :no_save, false
6
6
 
7
+ default_attr :output, 'compiled/doc/api'
8
+
7
9
  default_attr :title, 'Rails API Project'
8
10
  default_attr :url_title, 'my_app'
9
11
  default_attr :url_prefix, '/api'
@@ -20,8 +22,8 @@ module YARD::APIPlugin
20
22
 
21
23
  default_attr :one_file, false
22
24
  default_attr :strict, false
23
- default_attr :verbose, false
24
- default_attr :debug, false
25
+ default_attr :verbose, ENV['VERBOSE'] || false
26
+ default_attr :debug, ENV['DEBUG'] || false
25
27
  default_attr :theme, 'default'
26
28
 
27
29
  default_attr :tabular_arguments, false
@@ -39,6 +41,9 @@ module YARD::APIPlugin
39
41
  default_attr :show_footer, true
40
42
  default_attr :readme_page_title, 'Home'
41
43
 
44
+ default_attr :resource_index, false
45
+ default_attr :centered, true
46
+
42
47
  attr_accessor :readme
43
48
  end
44
49
  end
@@ -0,0 +1,9 @@
1
+ module YARD::APIPlugin
2
+ module Registry
3
+ def self.root
4
+ @root ||= begin
5
+ YARD::Registry.register YARD::CodeObjects::NamespaceObject.new(:root, :API)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,61 @@
1
+ module YARD::APIPlugin
2
+ class Serializer < ::YARD::Serializers::FileSystemSerializer
3
+ USNSEP = '__' # url-safe namespace separator
4
+ FSSEP = '/'
5
+
6
+ def self.topicize(str)
7
+ str.lines.first.gsub(/\W+/, '_').downcase
8
+ end
9
+
10
+ def serialize(object, data)
11
+ path = File.join(basepath, serialized_path(object))
12
+
13
+ if path.include?(' ')
14
+ debugger
15
+ end
16
+
17
+ log.debug "Serializing to #{path}"
18
+ File.open!(path, "wb") {|f| f.write data }
19
+ end
20
+
21
+ def serialized_path(object)
22
+ return object if object.is_a?(String)
23
+
24
+ fspath = nil
25
+
26
+ if object.is_a?(YARD::CodeObjects::ExtraFileObject)
27
+ fspath = 'file.' + object.name + (extension.empty? ? '' : ".#{extension}")
28
+ else
29
+ fspath = if object == YARD::Registry.root
30
+ "top-level-namespace"
31
+ else
32
+ self.class.topicize(get_api_id(object))
33
+ end
34
+
35
+ if object.is_a?(YARD::CodeObjects::MethodObject)
36
+ fspath += '_' + object.scope.to_s[0,1]
37
+ end
38
+
39
+ unless extension.empty?
40
+ fspath += ".#{extension}"
41
+ end
42
+ end
43
+
44
+ if (fspath.include?(' '))
45
+ debugger
46
+ end
47
+
48
+ fspath.gsub(/[^\w\.\-_\/]+/, '-')
49
+ end
50
+
51
+ def get_api_id(object)
52
+ if object[:api_id]
53
+ object.api_id
54
+ elsif tag = object.tag(:API)
55
+ tag.text.lines.first.strip
56
+ else
57
+ object.name.to_s
58
+ end
59
+ end
60
+ end
61
+ end
@@ -5,100 +5,100 @@ module YARD::Templates::Helpers::BaseHelper
5
5
  YARD::APIPlugin.options
6
6
  end
7
7
 
8
- def linkify_with_api(*args)
9
- # References to controller actions
10
- #
11
- # Syntax: api:ControllerName#method_name [TITLE OVERRIDE]
12
- #
13
- # @example Explicit reference with title defaulting to the action
14
- # # @see api:Assignments#create
15
- # # => <a href="assignments.html#method.assignments_api.create">create</a>
16
- #
17
- # @example Inline reference with an overriden title
18
- # # Here's a link to absolute {api:Assignments#destroy destruction}
19
- # # => <a href="assignments.html#method.assignments_api.destroy">destruction</a>
20
- #
21
- # @note Action links inside the All Resources section will be relative.
22
- if args.first.is_a?(String) && args.first =~ %r{^api:([^#]+)#(.*)}
23
- topic, controller = *lookup_topic($1.to_s)
24
- if topic
25
- html_file = "#{topicize topic.first}.html"
26
- action = $2
27
- link_url("#{html_file}#method.#{topicize(controller.name.to_s).sub("_controller", "")}.#{action}", args[1])
28
- else
29
- raise "couldn't find API link for #{args.first}"
30
- end
31
-
32
- # References to API objects defined by @object
33
- #
34
- # Syntax: api:ControllerName:Object+Name [TITLE OVERRIDE]
35
- #
36
- # @example Explicit resource reference with title defaulting to its name
37
- # # @see api:Assignments:Assignment
38
- # # => <a href="assignments.html#Assignment">Assignment</a>
39
- #
40
- # @example Explicit resource reference with an overriden title
41
- # # @return api:Assignments:AssignmentOverride An Assignment Override
42
- # # => <a href="assignments.html#Assignment">An Assignment Override</a>
43
- elsif args.first.is_a?(String) && args.first =~ %r{^api:([^:]+):(.*)}
44
- scope_name, resource_name = $1.downcase, $2.gsub('+', ' ')
45
- link_url("#{scope_name}.html##{resource_name}", args[1] || resource_name)
46
- elsif args.first.is_a?(String) && args.first == 'Appendix:' && args.size > 1
47
- __errmsg = "unable to locate referenced appendix '#{args[1]}'"
48
-
49
- unless appendix = lookup_appendix(args[1].to_s)
50
- raise __errmsg
51
- end
52
-
53
- topic, controller = *lookup_topic(appendix.namespace.to_s)
54
-
55
- if topic
56
- html_file = "#{topicize topic.first}.html"
57
- bookmark = "#{appendix.name.to_s.gsub(' ', '+')}-appendix"
58
- ret = link_url("#{html_file}##{bookmark}", appendix.title)
59
- else
60
- raise __errmsg
61
- end
62
-
63
- # A non-API link, delegate to YARD's HTML linker
64
- else
65
- linkify_without_api(*args)
66
- end
67
- end
68
-
69
- alias_method :linkify_without_api, :linkify
70
- alias_method :linkify, :linkify_with_api
71
-
72
- def lookup_topic(controller_name)
73
- controller = nil
74
- topic = options[:resources].find do |resource, controllers|
75
- controllers.detect do |_controller|
76
- if _controller.path.to_s == controller_name
77
- controller = _controller
78
- end
79
- end
80
- end
81
-
82
- [ topic, controller ]
83
- end
84
-
85
- def lookup_appendix(title)
86
- appendix = nil
87
-
88
- YARD::APIPlugin.log("Looking up appendix: #{title}") if api_options.verbose
89
-
90
- if object
91
- # try in the object scope
92
- appendix = YARD::Registry.at(".appendix.#{object.path}.#{title}")
93
-
94
- # try in the object's namespace scope
95
- if appendix.nil? && object.respond_to?(:namespace)
96
- appendix = YARD::Registry.at(".appendix.#{object.namespace.path}.#{title}")
97
- end
98
- end
99
-
100
- appendix
101
- end
8
+ # def linkify_with_api(*args)
9
+ # # References to controller actions
10
+ # #
11
+ # # Syntax: api:ControllerName#method_name [TITLE OVERRIDE]
12
+ # #
13
+ # # @example Explicit reference with title defaulting to the action
14
+ # # # @see api:Assignments#create
15
+ # # # => <a href="assignments.html#method.assignments_api.create">create</a>
16
+ # #
17
+ # # @example Inline reference with an overriden title
18
+ # # # Here's a link to absolute {api:Assignments#destroy destruction}
19
+ # # # => <a href="assignments.html#method.assignments_api.destroy">destruction</a>
20
+ # #
21
+ # # @note Action links inside the All Resources section will be relative.
22
+ # if args.first.is_a?(String) && args.first =~ %r{^api:([^#]+)#(.*)}
23
+ # topic, controller = *lookup_topic($1.to_s)
24
+ # if topic
25
+ # html_file = "#{topicize topic.first}.html"
26
+ # action = $2
27
+ # link_url("#{html_file}#method.#{topicize(controller.name.to_s).sub("_controller", "")}.#{action}", args[1])
28
+ # else
29
+ # raise "couldn't find API link for #{args.first}"
30
+ # end
31
+
32
+ # # References to API objects defined by @object
33
+ # #
34
+ # # Syntax: api:ControllerName:Object+Name [TITLE OVERRIDE]
35
+ # #
36
+ # # @example Explicit resource reference with title defaulting to its name
37
+ # # # @see api:Assignments:Assignment
38
+ # # # => <a href="assignments.html#Assignment">Assignment</a>
39
+ # #
40
+ # # @example Explicit resource reference with an overriden title
41
+ # # # @return api:Assignments:AssignmentOverride An Assignment Override
42
+ # # # => <a href="assignments.html#Assignment">An Assignment Override</a>
43
+ # elsif args.first.is_a?(String) && args.first =~ %r{^api:([^:]+):(.*)}
44
+ # scope_name, resource_name = $1.downcase, $2.gsub('+', ' ')
45
+ # link_url("#{scope_name}.html##{resource_name}", args[1] || resource_name)
46
+ # elsif args.first.is_a?(String) && args.first == 'Appendix:' && args.size > 1
47
+ # __errmsg = "unable to locate referenced appendix '#{args[1]}'"
48
+
49
+ # unless appendix = lookup_appendix(args[1].to_s)
50
+ # raise __errmsg
51
+ # end
52
+
53
+ # topic, controller = *lookup_topic(appendix.namespace.to_s)
54
+
55
+ # if topic
56
+ # html_file = "#{topicize topic.first}.html"
57
+ # bookmark = "#{appendix.name.to_s.gsub(' ', '+')}-appendix"
58
+ # ret = link_url("#{html_file}##{bookmark}", appendix.title)
59
+ # else
60
+ # raise __errmsg
61
+ # end
62
+
63
+ # # A non-API link, delegate to YARD's HTML linker
64
+ # else
65
+ # linkify_without_api(*args)
66
+ # end
67
+ # end
68
+
69
+ # alias_method :linkify_without_api, :linkify
70
+ # alias_method :linkify, :linkify_with_api
71
+
72
+ # def lookup_topic(controller_name)
73
+ # controller = nil
74
+ # topic = options[:resources].find do |resource, controllers|
75
+ # controllers.detect do |_controller|
76
+ # if _controller.path.to_s == controller_name
77
+ # controller = _controller
78
+ # end
79
+ # end
80
+ # end
81
+
82
+ # [ topic, controller ]
83
+ # end
84
+
85
+ # def lookup_appendix(title)
86
+ # appendix = nil
87
+
88
+ # YARD::APIPlugin.log("Looking up appendix: #{title}") if api_options.verbose
89
+
90
+ # if object
91
+ # # try in the object scope
92
+ # appendix = YARD::Registry.at(".appendix.#{object.path}.#{title}")
93
+
94
+ # # try in the object's namespace scope
95
+ # if appendix.nil? && object.respond_to?(:namespace)
96
+ # appendix = YARD::Registry.at(".appendix.#{object.namespace.path}.#{title}")
97
+ # end
98
+ # end
99
+
100
+ # appendix
101
+ # end
102
102
 
103
103
  def tag_partial(name, tag, locals={})
104
104
  options[:tag] = tag
@@ -121,4 +121,8 @@ module YARD::Templates::Helpers::BaseHelper
121
121
  def get_current_route
122
122
  get_current_routes.first
123
123
  end
124
+
125
+ def schema_is_model?(schema)
126
+ schema.has_key?('description') && schema.has_key?('properties')
127
+ end
124
128
  end
@@ -2,7 +2,7 @@ require 'yard/templates/helpers/html_helper'
2
2
 
3
3
  module YARD::Templates::Helpers::HtmlHelper
4
4
  def topicize(str)
5
- str.split("\n")[0].gsub(' ', '_').underscore
5
+ ::YARD::APIPlugin::Serializer.topicize(str)
6
6
  end
7
7
 
8
8
  def url_for_file(filename, anchor = nil)
@@ -11,6 +11,10 @@ module YARD::Templates::Helpers::HtmlHelper
11
11
  link
12
12
  end
13
13
 
14
+ # def url_for_api_object(name, object)
15
+ # "#{object.parent.path}::#{name}"
16
+ # end
17
+
14
18
  def static_pages()
15
19
  @@static_pages ||= begin
16
20
  locate_static_pages(YARD::APIPlugin.options)
@@ -133,4 +137,22 @@ module YARD::Templates::Helpers::HtmlHelper
133
137
  html = resolve_links(html)
134
138
  html
135
139
  end
140
+
141
+ def htmlify_tag_type(tag)
142
+ co = if tag.type =~ /API::(?:(\S+)::)?(\S+)\b/
143
+ # discard [] at the end denoting array of objects
144
+ P(tag.type.gsub(/\[\]$/, ''))
145
+ end
146
+
147
+ if co && !co.is_a?(YARD::CodeObjects::Proxy)
148
+ begin
149
+ linkify(co, tag.type.split(YARD::CodeObjects::NSEP)[1..-1].join(YARD::CodeObjects::NSEP))
150
+ rescue Exception => e
151
+ YARD::APIPlugin.logger.warn e
152
+ ""
153
+ end
154
+ else
155
+ tag.type
156
+ end
157
+ end
136
158
  end
@@ -24,6 +24,8 @@ module YARD
24
24
  case object.type
25
25
  when :root, :module, :constant
26
26
  false
27
+ when :api
28
+ true
27
29
  when :method, :class
28
30
  return false if object.tags('internal').any?
29
31
 
@@ -1,5 +1,5 @@
1
1
  module YARD
2
2
  module APIPlugin
3
- VERSION = "0.2.3"
3
+ VERSION = "0.3.0"
4
4
  end
5
5
  end
data/lib/yard-api.rb CHANGED
@@ -37,6 +37,10 @@ module YARD
37
37
  log.enter_level(level) { log.puts(message) }
38
38
  end
39
39
 
40
+ def self.logger
41
+ YARD::Logger.instance
42
+ end
43
+
40
44
  def self.on_error(message)
41
45
  if self.options.strict
42
46
  raise message
@@ -48,8 +52,11 @@ module YARD
48
52
 
49
53
  require 'yard-api/version'
50
54
  require 'yard-api/options'
55
+ require 'yard-api/registry'
51
56
  require 'yard-api/tags'
52
57
  require 'yard-api/verifier'
58
+ require 'yard-api/serializer'
59
+ require 'yard-api/code_objects/api_object'
53
60
  require 'yard-api/templates/helpers/base_helper'
54
61
  require 'yard-api/templates/helpers/html_helper'
55
62
  require 'yard-api/templates/helpers/route_helper'
@@ -63,6 +70,16 @@ module YARD
63
70
  class YardocOptions < Templates::TemplateOptions
64
71
  default_attr :resources, []
65
72
  default_attr :json_objects, []
73
+ default_attr :controllers, []
74
+ default_attr :current_route, nil
75
+ default_attr :inline_file, nil
76
+ default_attr :all_resources, nil
77
+ default_attr :tag, nil
78
+ default_attr :locale, nil
79
+ default_attr :multi_dialect, false
80
+ default_attr :argument_tags, []
81
+ default_attr :output, nil
82
+ default_attr :json_objects_map, {}
66
83
  end
67
84
  end
68
85
  end
@@ -0,0 +1,67 @@
1
+ require 'spec_helper'
2
+
3
+ module YARD
4
+ module Serializers
5
+ # A serializer that writes data to standard output.
6
+ class SpecSerializer < Base
7
+ # Creates a serializer to print text to stdout
8
+ #
9
+ # @param [Fixnum, nil] wrap if wrap is a number, wraps text to +wrap+
10
+ # columns, otherwise no wrapping is done.
11
+ def initialize(buffer)
12
+ @buffer = buffer
13
+ end
14
+
15
+ # Overrides serialize behaviour to write data to standard output
16
+ def serialize(object, data)
17
+ @buffer << data
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ describe YARD::Templates::Engine.template(:api, :fulldoc) do
24
+ before do
25
+ Registry.clear
26
+ set_option("output", "./tmp/doc")
27
+ end
28
+
29
+ describe '@object' do
30
+ it 'should register it as a CodeObject inside the API namespace' do
31
+ populate <<-'eof'
32
+ # @API Quizzes
33
+ #
34
+ # @object Quiz
35
+ # {
36
+ # "id": "Quiz",
37
+ # "description": "A quiz that can be taken by students.",
38
+ # "properties": {
39
+ # "id": {
40
+ # "type": "String"
41
+ # }
42
+ # }
43
+ # }
44
+ class QuizzesController < ApplicationController
45
+ end
46
+ eof
47
+
48
+ YARD::Templates::Engine.render({
49
+ objects: [ P('QuizzesController') ],
50
+ type: :fulldoc,
51
+ template: :api,
52
+ format: :html
53
+ })
54
+
55
+ expect(Registry.all.map(&:path).sort).to eq(%w[
56
+ API
57
+ API::Quiz
58
+ API::Quizzes
59
+ API::Quizzes::Quiz
60
+ QuizzesController
61
+ ])
62
+
63
+ expect(P('API::Quizzes::Quiz')).to be_truthy
64
+ expect(P('API::Quizzes::Quiz').parent).to eq P('API::Quizzes')
65
+ end
66
+ end
67
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'byebug'
2
+
1
3
  # This file was generated by the `rspec --init` command. Conventionally, all
2
4
  # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
5
  # The generated `.rspec` file contains `--require spec_helper` which will cause this
@@ -111,4 +113,15 @@ RSpec.configure do |config|
111
113
  @options.reset_defaults
112
114
  @overridden_options.clear
113
115
  end
116
+
117
+ Registry = YARD::Registry
118
+
119
+ def T(template)
120
+ YARD::Templates::Engine.template(:api)
121
+ end
122
+
123
+ def populate(str=nil)
124
+ Registry.clear
125
+ YARD.parse_string str if str
126
+ end
114
127
  end
@@ -24,11 +24,7 @@ a.active {
24
24
  }
25
25
 
26
26
  code {
27
- margin: 0 2px;
28
- padding: 0px 5px;
29
- border: 1px solid #ddd;
30
- background-color: #f8f8f8;
31
- border-radius: 3px;
27
+ background-color: #f5f5f5;
32
28
  font-family: Consolas, "Liberation Mono", Courier, monospace;
33
29
  font-weight: bold;
34
30
  }
@@ -47,13 +43,13 @@ h2 {
47
43
 
48
44
  .endpoint__beta-banner {
49
45
  border-left: 5px solid rgba(255,0,0,0.65);
50
- background-color: #ffc;
46
+ background-color: #FFFBE6;
51
47
  padding: 0.6em;
52
48
  font-size: 85%;
53
49
  }
54
50
 
55
51
  .method-details__beta-flag {
56
- color: #CC0000;
52
+ color: #800;
57
53
  background: #fca;
58
54
  padding: 0.5em 0.6em;
59
55
  font-size: 85%;
@@ -77,6 +73,8 @@ h2 {
77
73
  color: inherit;
78
74
  padding: 0.35em 0;
79
75
  text-decoration: none;
76
+ font-size: 85%;
77
+ line-height: 1.2;
80
78
  }
81
79
 
82
80
  #sidebar a:hover {
@@ -88,28 +86,31 @@ h2 {
88
86
  }
89
87
 
90
88
  .sidebar__heading {
91
- color: #939da3;
89
+ color: #aaa;
92
90
  text-transform: uppercase;
93
91
  font-size: 1em;
94
92
  margin-bottom: 0.5em;
95
93
  padding-bottom: 0;
96
94
  margin-top: 1em;
95
+ font-weight: normal;
97
96
  }
98
97
 
99
98
  .endpoint__path {
100
- font-size: 90%;
99
+ font-size: 85%;
101
100
  font-weight: normal;
102
101
  font-family: Monaco, Consolas, Courier, monospace;
103
- padding: 1em;
104
- margin-top: 1em;
105
- background: #ffc;
102
+ padding: 0.5em;
103
+ margin: 1em 0;
104
+ background: #FFFBE6;
106
105
  }
107
106
 
108
- .method-details .verb {
107
+ .method-details__verb {
109
108
  font-weight: bold;
109
+ color: #800;
110
110
  }
111
+
111
112
  .method-details .id-fragment {
112
- color: #CC0000;
113
+ color: #800;
113
114
  }
114
115
 
115
116
  .method-details__defined-in {
@@ -135,7 +136,7 @@ h2 {
135
136
  /* highlight.js overrides */
136
137
  pre.code, div.syntaxhighlighter {
137
138
  font-family: Monaco, Consolas, Courier, monospace;
138
- background-color: #ffd;
139
+ background-color: #FFFBE6;
139
140
  font-size: 85%;
140
141
  overflow: auto;
141
142
  padding: 0.5em 1em;
@@ -157,18 +158,29 @@ pre.code, div.syntaxhighlighter {
157
158
 
158
159
  td.code textarea { display: none; }
159
160
 
160
- table#quicklinks {
161
- width: 100%;
161
+ .topic__quicklinks {
162
+ padding: 0;
163
+ margin: 0;
162
164
  }
163
- table#quicklinks tr {
164
- border: 1px solid black;
165
+
166
+ .topic__quicklinks li {
167
+ list-style: none;
168
+ display: inline-block;
169
+ margin-right: 1em;
170
+ margin-bottom: 1em;
165
171
  }
166
- table#quicklinks td a {
167
- padding: 6px;
172
+
173
+ .topic__quicklinks li a {
174
+ display: block;
175
+ padding: 0.5em 1em;
176
+ background-color: #f5f5f5;
177
+ text-decoration: none;
178
+ border-radius: 3px;
179
+ font-size: 85%;
168
180
  }
169
- table#quicklinks th {
170
- font-weight: bold;
171
- padding: 12px;
181
+
182
+ .topic__quicklinks li a:hover {
183
+ background-color: transparent;
172
184
  }
173
185
 
174
186
  a.method-details__name-link {
@@ -285,9 +297,6 @@ h4 {
285
297
  }
286
298
 
287
299
  code.argument-listing__argument-name {
288
- border: none;
289
- padding: 0;
290
- margin: 0;
291
300
  overflow: auto;
292
301
  }
293
302
 
@@ -301,7 +310,7 @@ code.argument-listing__argument-name {
301
310
 
302
311
  .argument-listing__argument-required {
303
312
  font-style: normal;
304
- color: #CC0000;
313
+ color: #800;
305
314
  font-weight: bold;
306
315
  opacity: 0.6;
307
316
  }
@@ -319,7 +328,7 @@ code.argument-listing__argument-name {
319
328
 
320
329
  .example-codeblocks__tabs {
321
330
  margin-bottom: -0.5em; /* negate the margin of the example block */
322
- background: #ffc;
331
+ background: #FFF7CC;
323
332
  padding: 0.5em 1em;
324
333
  font-size: 85%;
325
334
  }
@@ -331,4 +340,44 @@ code.argument-listing__argument-name {
331
340
 
332
341
  .example-codeblocks__tabs a.active {
333
342
  font-weight: bold;
343
+ }
344
+
345
+ .type-mute {
346
+ color: #777;
347
+ }
348
+
349
+ .object-synopsis__header {
350
+ padding: 1em 0;
351
+ background: #FFFBE6;
352
+ position: relative; /* for the toggler btn */
353
+ }
354
+
355
+ .object-synopsis__header-text {
356
+ display: block;
357
+ width: 40%;
358
+ max-width: 260px;
359
+ text-align: right;
360
+ }
361
+
362
+ .object-synopsis__toggler {
363
+ appearance: none;
364
+ -webkit-appearance: none;
365
+ -moz-appearance: none;
366
+ border: none;
367
+ padding: 0;
368
+
369
+ background-color: transparent;
370
+ font-size: small;
371
+ color: #4183c4;
372
+ position: absolute;
373
+ top: 50%;
374
+ right: 1em;
375
+ margin-top: -0.5em;
376
+ cursor: pointer;
377
+ font-weight: bold;
378
+ line-height: 1;
379
+ }
380
+
381
+ .object-synopsis__toggler:hover {
382
+ text-decoration: underline;
334
383
  }
@@ -1,6 +1,35 @@
1
+ /* global $:true, hljs:true */
2
+
3
+ function makeObjectSynopsisBlocksTogglable() {
4
+ var MARKER_CLASS = 'object-synopsis__hidden';
5
+
6
+ $('.object-synopsis__toggler').each(function() {
7
+ var $togglerButton = $(this);
8
+ var $objectSynopsis = $togglerButton.closest('.object-synopsis');
9
+ var $objectSynopsisContent = $objectSynopsis.find('.object-synopsis__content');
10
+
11
+ function render() {
12
+ if (!$togglerButton.hasClass(MARKER_CLASS)) {
13
+ $togglerButton.text('Hide');
14
+ $objectSynopsisContent.show();
15
+ }
16
+ else {
17
+ $togglerButton.text('Show');
18
+ $objectSynopsisContent.hide();
19
+ }
20
+ }
21
+
22
+ $togglerButton.on('click', function() {
23
+ $togglerButton.toggleClass(MARKER_CLASS);
24
+ render();
25
+ });
26
+
27
+ render();
28
+ });
29
+ }
30
+
1
31
  $(function() {
2
32
  $('.method-details__name').each(function(i, el) {
3
- var subtopic = $(el).data('subtopic');
4
33
  var $a = $(el).find('a');
5
34
  var anchorText = $.trim($a[0].innerHTML);
6
35
 
@@ -8,9 +37,9 @@ $(function() {
8
37
  return;
9
38
  }
10
39
 
11
- var $row = $('#quicklinks');
40
+ var $row = $('#topicQuicklinks');
12
41
  var $link = $('<a/>', {
13
- href: '#'+$(el).attr('name')
42
+ href: '#' + $(el).attr('name')
14
43
  }).html(anchorText);
15
44
 
16
45
  $('<li>').append($link).appendTo($row);
@@ -50,7 +79,9 @@ $(function() {
50
79
  $(this).addClass('active');
51
80
  });
52
81
  });
53
-
82
+
54
83
  $(this).find('a:first').click();
55
84
  });
56
- });
85
+
86
+ makeObjectSynopsisBlocksTogglable();
87
+ });
@@ -1,11 +1,21 @@
1
1
  require 'pathname'
2
+ require 'json'
2
3
 
3
4
  include YARD::Templates::Helpers::ModuleHelper
4
5
  include YARD::Templates::Helpers::FilterHelper
5
6
 
6
7
  def init
8
+ YARD::APIPlugin.logger.info "YARD-API: starting."
9
+
10
+ options.serializer = YARD::APIPlugin::Serializer.new
11
+ options.serializer.basepath = api_options.output
12
+
13
+ options.objects.each do |object|
14
+ object[:api_id] = object.tag('API').text.lines.first
15
+ end
16
+
7
17
  options[:resources] = options[:objects].
8
- group_by { |o| o.tags('API').first.text.split("\n").first }.
18
+ group_by { |o| o[:api_id] }.
9
19
  sort_by { |o| o.first }
10
20
 
11
21
  build_json_objects_map
@@ -15,13 +25,51 @@ def init
15
25
  return serialize_onefile_index
16
26
  end
17
27
 
18
- serialize_index if File.exists?(api_options['readme'])
28
+ serialize_index if File.exists?(api_options['readme'] || '')
19
29
  serialize_static_pages
20
30
  serialize_resource_index if api_options['resource_index']
21
31
 
22
32
  options.delete(:objects)
23
33
 
24
34
  options[:resources].each do |resource, controllers|
35
+ controllers.each do |controller|
36
+ if controller.is_a?(YARD::CodeObjects::NamespaceObject)
37
+ co = YARD::CodeObjects::ClassObject.new(
38
+ YARD::APIPlugin::Registry.root,
39
+ controller[:api_id]
40
+ )
41
+
42
+ YARD::Registry.register co
43
+
44
+ (controller.tags(:object) + controller.tags(:model)).each do |tag|
45
+ tag_co = YARD::CodeObjects::APIObject.new(co, tag.text.lines[0].strip)
46
+ tag_co.object = tag.object
47
+
48
+ # Make an alias on the global API namespace, for convenience.
49
+ # Now an object called "Bar" under the "Foo" controller can be
50
+ # referenced using [API::Bar] as well as [API::Foo::Bar] which will
51
+ # never face any conflicts.
52
+ shortcut_tag_co = YARD::CodeObjects::APIObject.new(YARD::APIPlugin::Registry.root, tag.text.lines[0].strip)
53
+ shortcut_tag_co.object = tag.object
54
+
55
+ # We need to override #namespace because #url_for() uses it to
56
+ # generate the url, which has to be done usign #object and not
57
+ # #namespace (which points to P("API") and we want
58
+ # P("API::#{tag.object.path}")).
59
+ shortcut_tag_co.namespace = tag.object
60
+
61
+ YARD::Registry.register(tag_co)
62
+ YARD::Registry.register(shortcut_tag_co)
63
+ end
64
+ end
65
+ end
66
+
67
+ # debugger
68
+
69
+ if controllers.length > 1
70
+ debugger
71
+ end
72
+
25
73
  serialize_resource(resource, controllers)
26
74
  end
27
75
  end
@@ -34,12 +82,21 @@ def serialize(object)
34
82
  end
35
83
 
36
84
  def serialize_resource(resource, controllers)
85
+ YARD::APIPlugin.logger.info('=' * 80)
86
+ YARD::APIPlugin.logger.info ">>> #{resource} <<< (#{controllers})"
87
+ YARD::APIPlugin.logger.info('-' * 80)
88
+
37
89
  options[:object] = resource
38
90
  options[:controllers] = controllers
39
- Templates::Engine.with_serializer("#{topicize resource}.html", options[:serializer]) do
40
- T('layout').run(options)
91
+
92
+ controllers.each do |controller|
93
+ Templates::Engine.with_serializer(controller, options.serializer) do
94
+ T('layout').run(options)
95
+ end
41
96
  end
97
+
42
98
  options.delete(:controllers)
99
+ YARD::APIPlugin.logger.info('-' * 80)
43
100
  end
44
101
 
45
102
  def serialize_index
@@ -93,12 +150,55 @@ def build_json_objects_map
93
150
  options[:json_objects] = {}
94
151
  options[:resources].each do |r,cs|
95
152
  cs.each do |controller|
96
- (controller.tags(:object) + controller.tags(:model)).each do |obj|
97
- name, json = obj.text.split(%r{\n+}, 2).map(&:strip)
98
- options[:json_objects_map][name] = topicize r
99
- options[:json_objects][r] ||= []
100
- options[:json_objects][r] << [name, json]
153
+ (controller.tags(:object) + controller.tags(:model)).each do |tag|
154
+ name, json_string = tag.text.split(%r{\n+}, 2).map(&:strip)
155
+
156
+ if json = parse_json(json_string)
157
+ options[:json_objects_map][name] = topicize r
158
+ options[:json_objects][r] ||= []
159
+ options[:json_objects][r] << [name, json]
160
+ end
101
161
  end
102
162
  end
103
163
  end
104
164
  end
165
+
166
+ def parse_json(json_string)
167
+ JSON::parse(json_string || '').tap do |json|
168
+ validate_json_schema(json, ->(msg) {
169
+ raise <<-MSG
170
+ #{'=' * 32}
171
+ #{msg.strip}
172
+ #{'-' * 32}
173
+ Offending JSON belongs to: "#{name}" in "#{tag.object.path}".
174
+ #{'=' * 32}
175
+ MSG
176
+ })
177
+ end
178
+ rescue JSON::ParserError => e
179
+ YARD::APIPlugin.on_error(
180
+ <<-MSG
181
+ #{'*' * 32}
182
+ A @#{tag.tag_name} docstring contains invalid JSON.
183
+ Offending JSON belongs to "#{name}" in "#{tag.object.path}".
184
+ ---
185
+ #{tag}
186
+ ---
187
+ #{e}
188
+ #{'*' * 32}
189
+ MSG
190
+ )
191
+
192
+ nil
193
+ end
194
+
195
+ def validate_json_schema(schema, on_error)
196
+ if schema_is_model?(schema)
197
+ if !schema['description'].is_a?(String)
198
+ on_error.call <<-MSG
199
+ Expected "description" to be a String, got #{schema['description'].class}.
200
+ Value: #{schema['description'].to_json}
201
+ MSG
202
+ end
203
+ end
204
+ end
@@ -10,20 +10,21 @@
10
10
  body {
11
11
  margin: 0 auto;
12
12
  width: <%= content_width %>px;
13
+ padding-left: <%= sidebar_width %>px;
13
14
  }
14
-
15
+
15
16
  #sidebar {
16
17
  margin-left: -<%= sidebar_width %>px;
17
18
  width: <%= sidebar_width %>px;
18
19
  left: auto;
19
20
  }
20
-
21
+
21
22
  #content {
22
23
  padding-left: 3em;
23
24
  }
24
- <% else %>
25
+ <% else %>
25
26
  body {
26
- padding-left: <%= sidebar_width + api_options.spacer %>px;
27
+ padding-left: <%= sidebar_width + (api_options.spacer || 0) %>px;
27
28
  padding-right: <%= api_options.spacer %>px;
28
29
  }
29
30
 
@@ -21,7 +21,7 @@
21
21
  <% end %>
22
22
 
23
23
  <% options[:resources].each do |resource, controllers| %>
24
- <%= sidebar_link resource, "#{topicize(resource)}.html" %>
24
+ <%= sidebar_link resource, controllers.first %>
25
25
  <% end %>
26
26
  </nav>
27
27
  </div>
@@ -7,7 +7,10 @@
7
7
  %>
8
8
 
9
9
  <div class='endpoint__path'>
10
- <span class="verb <%= route[:verb].downcase %>"><%= route[:verb] %></span>
10
+ <span class="method-details__verb <%= route[:verb].downcase %>">
11
+ <%= route[:verb] %>
12
+ </span>
13
+
11
14
  <%= formatted_route_path %>
12
15
  </div>
13
16
 
@@ -1,11 +1,13 @@
1
1
  <ul class="argument-listing">
2
- <% @argument_tags.each do |tag| %>
2
+ <% options[:argument_tags].each do |tag| %>
3
3
  <li class="argument-listing__argument">
4
4
  <div class="argument-listing__argument-details">
5
5
  <code class="argument-listing__argument-name"><%= h tag.name %></code>
6
- <span class="argument-listing__argument-type"><%= tag.type %></span>
6
+ <span class="argument-listing__argument-type">
7
+ <%= htmlify_tag_type(tag) %>
8
+ </span>
7
9
 
8
- <% if (tag.accepted_values || []).any? %>
10
+ <% if Array(tag.accepted_values).any? %>
9
11
  <span class="argument-listing__argument-values">
10
12
  <span>[ <%= tag.accepted_values.join(', ') %> ]</span>
11
13
  </span>
@@ -19,6 +21,8 @@
19
21
  <div class="argument-listing__argument-text">
20
22
  <% if !tag.text.empty? %>
21
23
  <%= html_markup_markdown(tag.text) %>
24
+ <% else %>
25
+ <em class="type-mute">No description provided.</em>
22
26
  <% end %>
23
27
  </div>
24
28
  </li>
@@ -1,4 +1,5 @@
1
- <% has_accepted_values = @argument_tags.any? { |e| Array(e.accepted_values).any? } %>
1
+ <% argument_tags = options[:argument_tags] %>
2
+ <% has_accepted_values = argument_tags.any? { |e| Array(e.accepted_values).any? } %>
2
3
 
3
4
  <table>
4
5
  <thead>
@@ -12,7 +13,7 @@
12
13
  </thead>
13
14
 
14
15
  <tbody>
15
- <% @argument_tags.each do |tag| %>
16
+ <% argument_tags.each do |tag| %>
16
17
  <tr>
17
18
  <td><code class="argument-name"><%= h tag.name %></code></td>
18
19
  <td><span class="argument-type"><%= tag.type %></span></td>
@@ -4,6 +4,7 @@
4
4
  <% else %>
5
5
  Returns <%= %w[a e i o u].include?(@resource_name[0]) ? 'an' : 'a' %>
6
6
  <% end %>
7
+
7
8
  <a href='<%= @resource_name %>.html#<%= @object_name %>'>
8
9
  <%= @is_list ? @object_name.pluralize : @object_name %>
9
10
  </a>
@@ -47,10 +47,10 @@ def emits
47
47
  end
48
48
 
49
49
  def argument
50
- # generic_tag :argument, :no_types => false, :label => "Request Parameters"
51
- @argument_tags = object.tags(:argument)
50
+ argument_tags = object.tags(:argument)
52
51
 
53
- if @argument_tags.any?
52
+ if argument_tags.any?
53
+ options[:argument_tags] = argument_tags
54
54
  erb('argument')
55
55
  end
56
56
  end
@@ -66,6 +66,11 @@ def returns
66
66
  @object_name = response_info.text.strip
67
67
  @is_list = false
68
68
  end
69
+
70
+ # if @object_name =~ /\{(\S+)\}/
71
+ # @object_name = $1
72
+ # end
73
+
69
74
  @resource_name = options[:json_objects_map][@object_name]
70
75
  return unless @resource_name
71
76
  erb(:returns)
@@ -4,17 +4,18 @@ def init
4
4
  sections :header, [:topic_doc, :method_details_list, [T('method_details')]]
5
5
  @resource = object
6
6
  @beta = options[:controllers].any? { |c| c.tag('beta') }
7
+ @meths = options[:controllers].map { |c| c.meths(:inherited => false, :included => false) }.flatten
8
+ @meths = run_verifier(@meths)
7
9
  end
8
10
 
9
11
  def method_details_list
10
- @meths = options[:controllers].map { |c| c.meths(:inherited => false, :included => false) }.flatten
11
- @meths = run_verifier(@meths)
12
12
  erb(:method_details_list)
13
13
  end
14
14
 
15
15
  def topic_doc
16
16
  @docstring = options[:controllers].map { |c| c.docstring }.join("\n\n")
17
17
  @object = @object.dup
18
+ @controller = options[:controllers].first
18
19
  def @object.source_type; nil; end
19
20
  @json_objects = options[:json_objects][@resource] || []
20
21
  erb(:topic_doc)
@@ -28,6 +29,21 @@ def properties_of_model(json)
28
29
  end
29
30
  end
30
31
 
32
+ def properties_of_model_as_tags(json)
33
+ props = json.has_key?('properties') ? json['properties'] : json
34
+ props.reduce([]) do |tags, (id, prop)|
35
+ is_required = prop.has_key?('required') ? prop['required'] : false
36
+ is_required_str = is_required ? 'Required' : 'Optional'
37
+
38
+ tag = YARD::APIPlugin::Tags::ArgumentTag.new(
39
+ nil,
40
+ "[#{is_required_str}, #{prop['type']}] #{id}\n #{prop['description']}"
41
+ )
42
+
43
+ tags << tag
44
+ end
45
+ end
46
+
31
47
  def word_wrap(text, col_width=80)
32
48
  text.gsub!( /(\S{#{col_width}})(?=\S)/, '\1 ' )
33
49
  text.gsub!( /(.{1,#{col_width}})(?:\s+|$)/, "\\1\n" )
@@ -13,15 +13,50 @@
13
13
  <section>
14
14
  <h2>Interfaces</h2>
15
15
 
16
- <ul id="quicklinks"></ul>
16
+ <% if @meths.empty? %>
17
+ <p>
18
+ <em class="type-mute">This API currently has no endpoints available.</em>
19
+ </p>
20
+ <% end %>
21
+
22
+ <ul id="topicQuicklinks" class="topic__quicklinks"></ul>
17
23
  </section>
18
24
 
19
- <% @json_objects.each do |name, json| %>
20
- <% properties = render_properties(json) %>
21
- <div class='object_definition'>
22
- <h3>
23
- <a name="<%= name %>"></a><%= name %> object synopsis:
24
- </h3>
25
- <pre class="example code"><%= html_syntax_highlight(properties ? properties : json, :plain) %></pre>
26
- </div>
27
- <% end %>
25
+ <% if @json_objects.any? %>
26
+ <section>
27
+ <h2>Object Synopses</h2>
28
+
29
+ <% @json_objects.each do |name, json| %>
30
+ <% if false %>
31
+ <% properties = render_properties(json) %>
32
+ <div class='object-synopsis'>
33
+ <h3>
34
+ <a name="<%= name %>"></a><%= name %> object synopsis:
35
+ </h3>
36
+ <pre class="example code"><%= html_syntax_highlight(properties ? properties : json, :plain) %></pre>
37
+ </div>
38
+ <% end %>
39
+
40
+ <div class="object-synopsis">
41
+ <h3 class="object-synopsis__header" id="<%= name %>-api">
42
+ <span class="object-synopsis__header-text"><%= name %></span>
43
+ <button class="object-synopsis__toggler"></button>
44
+ </h3>
45
+
46
+ <div class="object-synopsis__content">
47
+ <div class="object-synopsis__content-description">
48
+ <% if schema_is_model?(json) && !json['description'].empty? %>
49
+ <%= htmlify json['description'] %>
50
+ <% end %>
51
+ </div>
52
+
53
+ <%=
54
+ tag_partial("../../tags/html/argument/_list", nil, {
55
+ argument_tags: properties_of_model_as_tags(json)
56
+ })
57
+ %>
58
+ </div>
59
+ </div>
60
+ <% end %>
61
+ </section>
62
+ <% end %>
data/yard-api.gemspec CHANGED
@@ -19,4 +19,5 @@ Gem::Specification.new do |s|
19
19
  s.add_dependency 'yard-appendix'
20
20
  s.add_development_dependency 'rspec'
21
21
  s.add_development_dependency 'gem-release'
22
+ s.add_development_dependency 'byebug'
22
23
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yard-api
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ahmad Amireh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-07-28 00:00:00.000000000 Z
11
+ date: 2015-07-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: yard
@@ -66,6 +66,20 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: byebug
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
69
83
  description: |2
70
84
  TBD
71
85
  email: ahmad@instructure.com
@@ -78,9 +92,12 @@ files:
78
92
  - README.md
79
93
  - config/yard_api.yml
80
94
  - lib/yard-api.rb
95
+ - lib/yard-api/code_objects/api_object.rb
81
96
  - lib/yard-api/markup/redcarpet.rb
82
97
  - lib/yard-api/options.rb
83
98
  - lib/yard-api/railtie.rb
99
+ - lib/yard-api/registry.rb
100
+ - lib/yard-api/serializer.rb
84
101
  - lib/yard-api/tags.rb
85
102
  - lib/yard-api/tags/argument_tag.rb
86
103
  - lib/yard-api/templates/helpers/base_helper.rb
@@ -89,6 +106,7 @@ files:
89
106
  - lib/yard-api/verifier.rb
90
107
  - lib/yard-api/version.rb
91
108
  - lib/yard-api/yardoc_task.rb
109
+ - spec/fulldoc_spec.rb
92
110
  - spec/spec_helper.rb
93
111
  - spec/tags/argument_spec.rb
94
112
  - tasks/yard_api.rake