betterdocs 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +5 -0
  3. data/.gitignore +11 -0
  4. data/.rspec +2 -0
  5. data/.travis.yml +9 -0
  6. data/COPYING +202 -0
  7. data/Gemfile +8 -0
  8. data/LICENSE +202 -0
  9. data/README.md +124 -0
  10. data/Rakefile +25 -0
  11. data/VERSION +1 -0
  12. data/betterdocs.gemspec +48 -0
  13. data/lib/betterdocs.rb +27 -0
  14. data/lib/betterdocs/controller_collector.rb +50 -0
  15. data/lib/betterdocs/dsl.rb +9 -0
  16. data/lib/betterdocs/dsl/common.rb +26 -0
  17. data/lib/betterdocs/dsl/controller.rb +9 -0
  18. data/lib/betterdocs/dsl/controller/action.rb +126 -0
  19. data/lib/betterdocs/dsl/controller/action/param.rb +25 -0
  20. data/lib/betterdocs/dsl/controller/action/response.rb +47 -0
  21. data/lib/betterdocs/dsl/controller/controller.rb +31 -0
  22. data/lib/betterdocs/dsl/controller/controller_base.rb +21 -0
  23. data/lib/betterdocs/dsl/json_params.rb +8 -0
  24. data/lib/betterdocs/dsl/json_params/param.rb +31 -0
  25. data/lib/betterdocs/dsl/json_type_mapper.rb +27 -0
  26. data/lib/betterdocs/dsl/naming.rb +32 -0
  27. data/lib/betterdocs/dsl/representer.rb +29 -0
  28. data/lib/betterdocs/dsl/result.rb +10 -0
  29. data/lib/betterdocs/dsl/result/collection_property.rb +9 -0
  30. data/lib/betterdocs/dsl/result/link.rb +37 -0
  31. data/lib/betterdocs/dsl/result/property.rb +53 -0
  32. data/lib/betterdocs/generator/config_shortcuts.rb +28 -0
  33. data/lib/betterdocs/generator/markdown.rb +151 -0
  34. data/lib/betterdocs/generator/markdown/templates/README.md.erb +9 -0
  35. data/lib/betterdocs/generator/markdown/templates/section.md.erb +132 -0
  36. data/lib/betterdocs/global.rb +143 -0
  37. data/lib/betterdocs/json_params_representer.rb +37 -0
  38. data/lib/betterdocs/json_params_representer_collector.rb +48 -0
  39. data/lib/betterdocs/mix_into_controller.rb +19 -0
  40. data/lib/betterdocs/rake_tasks.rb +5 -0
  41. data/lib/betterdocs/representer.rb +42 -0
  42. data/lib/betterdocs/result_representer.rb +68 -0
  43. data/lib/betterdocs/result_representer_collector.rb +82 -0
  44. data/lib/betterdocs/section.rb +6 -0
  45. data/lib/betterdocs/tasks/doc.rake +55 -0
  46. data/lib/betterdocs/version.rb +8 -0
  47. data/spec/controller_dsl_spec.rb +143 -0
  48. data/spec/generator/markdown_spec.rb +5 -0
  49. data/spec/json_params_representer_spec.rb +79 -0
  50. data/spec/json_type_mapper_spec.rb +33 -0
  51. data/spec/result_representer_dsl_spec.rb +183 -0
  52. data/spec/result_representer_spec.rb +182 -0
  53. data/spec/spec_helper.rb +19 -0
  54. metadata +234 -0
@@ -0,0 +1,25 @@
1
+ class Betterdocs::Dsl::Controller::Action::Param
2
+ extend Tins::DSLAccessor
3
+ include ::Betterdocs::Dsl::Common
4
+
5
+ def initialize(param_name, &block)
6
+ name param_name
7
+ block and instance_eval(&block)
8
+ end
9
+
10
+ dsl_accessor :name
11
+
12
+ dsl_accessor :value
13
+
14
+ dsl_accessor :required, true
15
+
16
+ dsl_accessor :description, 'TODO'
17
+
18
+ dsl_accessor :use_in_url, true
19
+
20
+ alias use_in_url? use_in_url
21
+
22
+ def to_s
23
+ value
24
+ end
25
+ end
@@ -0,0 +1,47 @@
1
+ class Betterdocs::Dsl::Controller::Action::Response
2
+ include Betterdocs::Dsl::Common
3
+ extend Tins::DSLAccessor
4
+
5
+ def initialize(name = :default, &block)
6
+ @name = name.to_sym
7
+ @data_block = block || proc {}
8
+ end
9
+
10
+ dsl_accessor :name
11
+
12
+ def params
13
+ -> name { param(name).full?(:value) }
14
+ end
15
+
16
+ def data
17
+ @data ||= instance_eval(&@data_block)
18
+ end
19
+
20
+ def properties
21
+ representer.full? { |r| r.docs.nested_properties } || []
22
+ end
23
+
24
+ def links
25
+ representer.full? { |r| r.docs.nested_links } || []
26
+ end
27
+
28
+ def representer
29
+ if data
30
+ data.ask_and_send(:representer) ||
31
+ data.singleton_class.ancestors.find { |c|
32
+ Betterdocs::ResultRepresenter >= c && c.respond_to?(:docs)
33
+ # Actually it's more like
34
+ # Betterdocs::ResultRepresenter >= c && !c.singleton_class?
35
+ # in newer rubies.
36
+ # But singleton_class? is broken and private in ruby 2.1.x not
37
+ # existant in <= ruby 2.0.x and finally works in ruby 2.2.x.
38
+ # What a mess!
39
+ }
40
+ end
41
+ end
42
+
43
+ def to_json(*a)
44
+ my_data = data.ask_and_send(:to_hash) || data
45
+ my_data.to_json(*a)
46
+ end
47
+ end
@@ -0,0 +1,31 @@
1
+ require 'betterdocs/dsl/controller/controller_base'
2
+
3
+ class Betterdocs::Dsl::Controller::Controller < Betterdocs::Dsl::Controller::ControllerBase
4
+ def name
5
+ @name ||= controller.to_s.underscore.sub(/_controller\z/, '').to_sym
6
+ end
7
+
8
+ dsl_accessor :section
9
+
10
+ dsl_accessor :description, 'TODO'
11
+
12
+ def url
13
+ Betterdocs::Global.url_for(
14
+ controller: name,
15
+ action: :index,
16
+ format: 'json'
17
+ )
18
+ end
19
+
20
+ def url_helpers
21
+ Betterdocs::Global.url_helpers
22
+ end
23
+
24
+ def to_s
25
+ [ controller, '', "url: #{url}", '', description, '' ] * "\n"
26
+ end
27
+
28
+ def add_to_collector(collector)
29
+ collector.controller = self
30
+ end
31
+ end
@@ -0,0 +1,21 @@
1
+ require 'betterdocs/dsl/common'
2
+
3
+ class Betterdocs::Dsl::Controller::ControllerBase
4
+ include Betterdocs::Dsl::Common
5
+
6
+ def self.inherited(klass)
7
+ klass.class_eval { extend Tins::DSLAccessor }
8
+ end
9
+
10
+ def initialize(controller, &block)
11
+ controller(controller)
12
+ set_context controller
13
+ instance_eval(&block)
14
+ end
15
+
16
+ dsl_accessor :controller
17
+
18
+ def add_to_collector(collector)
19
+ raise NotImplementedError, 'add_to_collector needs to be implemented in subclass'
20
+ end
21
+ end
@@ -0,0 +1,8 @@
1
+ module Betterdocs
2
+ module Dsl
3
+ module JsonParams
4
+ end
5
+ end
6
+ end
7
+
8
+ require 'betterdocs/dsl/json_params/param'
@@ -0,0 +1,31 @@
1
+ class Betterdocs::Dsl::JsonParams::Param < Betterdocs::Dsl::Representer
2
+ extend Tins::DSLAccessor
3
+ include Betterdocs::Dsl::Common
4
+ include Betterdocs::Dsl::Naming
5
+
6
+ dsl_accessor :description, 'TODO'
7
+
8
+ dsl_accessor :value, 'TODO'
9
+
10
+ dsl_accessor :types do [] end
11
+
12
+ dsl_accessor :required, true
13
+
14
+ def initialize(representer, name, options, &block)
15
+ super
16
+ types Betterdocs::Dsl::JsonTypeMapper.map_types(types)
17
+ end
18
+
19
+ def assign(result, object)
20
+ assign?(object) or return
21
+ result[name] = compute_value(object)
22
+ end
23
+
24
+ def compute_value(object)
25
+ object.__send__(name)
26
+ end
27
+
28
+ def add_to_collector(collector)
29
+ collector.params[name] = self
30
+ end
31
+ end
@@ -0,0 +1,27 @@
1
+ module Betterdocs::Dsl::JsonTypeMapper
2
+ module_function
3
+
4
+ def derive_json_type_from(klass)
5
+ Class === klass or klass = klass.class
6
+ result = {
7
+ TrueClass => 'boolean',
8
+ FalseClass => 'boolean',
9
+ NilClass => 'null',
10
+ Numeric => 'number',
11
+ Array => 'array',
12
+ Hash => 'object',
13
+ String => 'string',
14
+ }.find { |match_class, json_type|
15
+ match_class >= klass and break json_type
16
+ } || 'undefined'
17
+ end
18
+
19
+ def map_types(types)
20
+ if Array === types and types.empty?
21
+ types = [ types ]
22
+ else
23
+ types = Array(types)
24
+ end
25
+ types.map { |t| derive_json_type_from(t) }.uniq.sort
26
+ end
27
+ end
@@ -0,0 +1,32 @@
1
+ module Betterdocs::Dsl::Naming
2
+ def initialize(*)
3
+ super
4
+ @options ||= {}
5
+ @below_path = []
6
+ end
7
+
8
+ attr_reader :options
9
+
10
+ def path
11
+ @below_path + [ public_name ]
12
+ end
13
+
14
+ def below_path(path)
15
+ dup.instance_eval do
16
+ @below_path = path
17
+ self
18
+ end
19
+ end
20
+
21
+ def public_name
22
+ @options[:as] || name
23
+ end
24
+
25
+ def full_name
26
+ path * '.'
27
+ end
28
+
29
+ def nesting_name
30
+ @below_path * '.'
31
+ end
32
+ end
@@ -0,0 +1,29 @@
1
+ class Betterdocs::Dsl::Representer
2
+ def initialize(representer, name, options, &block)
3
+ set_context @representer = representer
4
+ @name = name.to_sym
5
+ @options = options | {
6
+ if: -> { true },
7
+ unless: -> { false },
8
+ }
9
+ block and instance_eval(&block)
10
+ end
11
+
12
+ attr_reader :name
13
+
14
+ attr_reader :representer
15
+
16
+ def assign?(object)
17
+ object.instance_exec(&@options[:if]) &&
18
+ !object.instance_exec(&@options[:unless])
19
+ end
20
+
21
+ def assign(result, object)
22
+ raise NotImplementedError, 'assign needs to be implemented in subclass'
23
+ end
24
+
25
+ def add_to_collector(collector)
26
+ raise NotImplementedError, 'add_to_collector needs to be implemented in subclass'
27
+ end
28
+ end
29
+
@@ -0,0 +1,10 @@
1
+ module Betterdocs
2
+ module Dsl
3
+ module Result
4
+ end
5
+ end
6
+ end
7
+
8
+ require 'betterdocs/dsl/result/property'
9
+ require 'betterdocs/dsl/result/collection_property'
10
+ require 'betterdocs/dsl/result/link'
@@ -0,0 +1,9 @@
1
+ require 'betterdocs/dsl/result/property'
2
+
3
+ class Betterdocs::Dsl::Result::CollectionProperty < Betterdocs::Dsl::Result::Property
4
+ def compute_value(object)
5
+ object.__send__(name).to_a.compact.map do |v|
6
+ represent_with.hashify(v)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,37 @@
1
+ require 'betterdocs/dsl/representer'
2
+ require 'betterdocs/dsl/common'
3
+ require 'betterdocs/dsl/naming'
4
+
5
+ class Betterdocs::Dsl::Result::Link < Betterdocs::Dsl::Representer
6
+ extend Tins::DSLAccessor
7
+ include Betterdocs::Dsl::Common
8
+ include Betterdocs::Dsl::Naming
9
+
10
+ dsl_accessor :description, 'TODO'
11
+
12
+ dsl_accessor :templated, false
13
+
14
+ def url(&block)
15
+ if block
16
+ @url = block
17
+ elsif @url
18
+ @url
19
+ else
20
+ raise ArgumentError, 'link requires an URL'
21
+ end
22
+ end
23
+
24
+ def assign(result, object)
25
+ assign?(object) or return
26
+ link = {
27
+ 'rel' => name.to_s,
28
+ 'href' => object.instance_eval(&url).to_s,
29
+ }
30
+ templated and link['templated'] = true
31
+ result['links'].push(link)
32
+ end
33
+
34
+ def add_to_collector(collector)
35
+ collector.links[name] = self
36
+ end
37
+ end
@@ -0,0 +1,53 @@
1
+ require 'betterdocs/dsl/representer'
2
+ require 'betterdocs/dsl/common'
3
+ require 'betterdocs/dsl/naming'
4
+ require 'betterdocs/dsl/json_type_mapper'
5
+
6
+ class Betterdocs::Dsl::Result::Property < Betterdocs::Dsl::Representer
7
+ extend Tins::DSLAccessor
8
+ include Betterdocs::Dsl::Common
9
+ include Betterdocs::Dsl::Naming
10
+
11
+ dsl_accessor :represent_with
12
+
13
+ dsl_accessor :description, 'TODO'
14
+
15
+ dsl_accessor :example, 'TODO'
16
+
17
+ dsl_accessor :types do [] end
18
+
19
+ def initialize(representer, name, options, &block)
20
+ super
21
+ types Betterdocs::Dsl::JsonTypeMapper.map_types(types)
22
+ if sr = sub_representer?
23
+ sr < Betterdocs::ResultRepresenter or
24
+ raise TypeError, "#{sr.inspect} is not a Betterdocs::Result subclass"
25
+ end
26
+ end
27
+
28
+ def sub_representer?
29
+ represent_with
30
+ end
31
+
32
+ def actual_property_name
33
+ (options[:as] || name).to_s
34
+ end
35
+
36
+ def assign(result, object)
37
+ assign?(object) or return
38
+ result[actual_property_name] = compute_value(object)
39
+ end
40
+
41
+ def compute_value(object)
42
+ value = object.__send__(name)
43
+ if !value.nil? && represent_with
44
+ represent_with.hashify(value)
45
+ else
46
+ value
47
+ end
48
+ end
49
+
50
+ def add_to_collector(collector)
51
+ collector.properties[name] = self
52
+ end
53
+ end
@@ -0,0 +1,28 @@
1
+ module Betterdocs
2
+ module Generator
3
+ module ConfigShortcuts
4
+ def config
5
+ Betterdocs::Global.config
6
+ end
7
+
8
+ def project_name
9
+ config.project_name
10
+ end
11
+
12
+ def sections
13
+ Dir[config.api_controllers.to_s].each(&method(:load))
14
+ config.sections
15
+ end
16
+
17
+ def section(name)
18
+ name = name.to_sym
19
+ config.section(name) or STDERR.puts "Section #{name.inspect} does not exist: Link in readme file won't work."
20
+ "sections/#{name}"
21
+ end
22
+
23
+ def api_base_url
24
+ config
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,151 @@
1
+ module Betterdocs
2
+ module Generator
3
+ class Markdown
4
+ include ::Betterdocs::Generator::ConfigShortcuts
5
+ require 'fileutils'
6
+ include FileUtils::Verbose
7
+ require 'term/ansicolor'
8
+ include Term::ANSIColor
9
+
10
+ def initialize(only: nil)
11
+ only and @only = Regexp.new(only)
12
+ end
13
+
14
+ def generate
15
+ if dir = config.output_directory.full?
16
+ generate_to dir
17
+ else
18
+ fail "Specify an output_directory in your configuration!"
19
+ end
20
+ end
21
+
22
+ def generate_to(dirname)
23
+ configure_for_creation
24
+ prepare_dir dirname
25
+ create_sections(dirname)
26
+ create_readme dirname
27
+ create_assets
28
+ self
29
+ end
30
+
31
+ def configure_for_creation
32
+ STDERR.puts "Setting asset_host to #{Betterdocs::Global.asset_host.inspect}."
33
+ Betterdocs.rails.configuration.action_controller.asset_host = Betterdocs::Global.asset_host
34
+ options = {
35
+ host: Betterdocs::Global.api_host,
36
+ protocol: Betterdocs::Global.api_protocol
37
+ }
38
+ STDERR.puts "Setting default_url_options to #{options.inspect}."
39
+ Betterdocs.rails.application.routes.default_url_options = options
40
+ self
41
+ end
42
+
43
+ def create_sections(dirname)
44
+ cd dirname do
45
+ for section in sections.values
46
+ if @only
47
+ @only =~ section.name or next
48
+ end
49
+ STDERR.puts on_color(33, "Creating section #{section.name.inspect}.")
50
+ render_to "sections/#{section.name}.md", section_template, section.instance_eval('binding')
51
+ end
52
+ end
53
+ self
54
+ end
55
+
56
+ def create_readme(dirname)
57
+ name = 'README.md'
58
+ cd dirname do
59
+ STDERR.puts on_color(33, "Creating readme.")
60
+ render_to name, readme_template, binding
61
+ end
62
+ self
63
+ end
64
+
65
+ def create_assets
66
+ config.each_asset do |src, dst|
67
+ STDERR.puts on_color(33, "Creating asset #{dst.inspect} from #{src.inspect}.")
68
+ mkdir_p File.dirname(dst)
69
+ cp src, dst
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def fail_while_rendering(template, exception)
76
+ message = blink(color(231, on_color(
77
+ 124, " *** ERROR #{exception.class}: #{exception.message.inspect} in template ***")))
78
+ STDERR.puts message
79
+ Timeout.timeout(5, Timeout::Error) do
80
+ STDERR.print "Output long error message? (yes/NO) "
81
+ if STDIN.gets =~ /\Ay/i
82
+ STDERR.puts color(88, on_color(136, template)), message,
83
+ color(136, (%w[Backtrace:] + exception.backtrace) * "\n"),
84
+ message
85
+ end
86
+ end
87
+ rescue Timeout::Error
88
+ STDERR.puts "Nope…"
89
+ ensure
90
+ exit 1
91
+ end
92
+
93
+ def render_to(filename, template, binding)
94
+ File.open(filename, 'w') do |output|
95
+ rendered = ERB.new(template, nil, '-').result(binding)
96
+ output.write rendered
97
+ end
98
+ self
99
+ rescue => e
100
+ fail_while_rendering(template, e)
101
+ end
102
+
103
+ def default_templates_directory
104
+ File.join File.dirname(__FILE__), 'markdown', 'templates'
105
+ end
106
+
107
+ def read_template(filename)
108
+ STDERR.puts "Now reading #{filename.inspect}."
109
+ File.read(filename)
110
+ end
111
+
112
+ def provide_template(template_subpath)
113
+ if templates_directory = config.full?(:templates_directory)
114
+ path = File.expand_path(template_subpath, templates_directory)
115
+ File.file?(path) and return read_template(path)
116
+ end
117
+ path = File.expand_path(template_subpath, default_templates_directory)
118
+ File.file?(path) and return read_template(path)
119
+ message = "#{template_subpath.inspect} missing"
120
+ STDERR.puts " *** #{message}"
121
+ "[#{message}]"
122
+ end
123
+
124
+ def readme_template
125
+ provide_template 'README.md.erb'
126
+ end
127
+ memoize_method :readme_template
128
+
129
+ def section_template
130
+ provide_template 'section.md.erb'
131
+ end
132
+ memoize_method :section_template
133
+
134
+ def prepare_dir(dirname)
135
+ dirname.present? or raise ArgumentError,
136
+ "#{dirname.inspect} should be an explicite output dirname"
137
+ begin
138
+ stat = File.stat(dirname)
139
+ if stat.directory?
140
+ rm_rf Dir[dirname.to_s + '/**/*']
141
+ else
142
+ raise ArgumentError, "#{dirname.inspect} is not a directory"
143
+ end
144
+ rescue Errno::ENOENT
145
+ end
146
+ mkdir_p "#{dirname}/sections"
147
+ self
148
+ end
149
+ end
150
+ end
151
+ end