generate_api_coverage.rb 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. require 'bundler/setup'
  2. require 'json'
  3. class Member
  4. def initialize(json)
  5. @kind = json['kind']
  6. @name = json['name']
  7. @canonical_reference = json['canonicalReference']
  8. @members = json['members']
  9. end
  10. attr_reader :kind, :name, :canonical_reference
  11. INSPECT_ALLOW_LIST = ['kind', 'canonicalReference', 'name']
  12. def inspect
  13. {
  14. kind: @kind,
  15. name: @name,
  16. canonical_reference: @canonical_reference,
  17. members: @members&.map { |member| member.select { |key, _| INSPECT_ALLOW_LIST.include?(key) } },
  18. }.compact.inspect
  19. end
  20. def class?
  21. @kind == 'Class'
  22. end
  23. def method?
  24. @kind == 'Method' || @kind == 'Function'
  25. end
  26. def property?
  27. @kind == 'Variable' && @name =~ /^[a-z]/
  28. end
  29. def members
  30. @__members ||= @members.map do |json|
  31. Member.new(json)
  32. end
  33. end
  34. end
  35. class ApiDocJsonParser
  36. def initialize(raw_doc)
  37. json = JSON.parse(raw_doc)
  38. @root = Member.new(json)
  39. end
  40. def puppeteer_doc
  41. ClassDoc.new('Puppeteer', method_docs_for(puppeteer_entrypoint))
  42. end
  43. def class_docs
  44. puppeteer_entrypoint.members.filter_map do |member|
  45. ClassDoc.new(member.name, method_docs_for(member)) if member.class?
  46. end
  47. end
  48. private def puppeteer_entrypoint
  49. @root.members.first
  50. end
  51. private def method_docs_for(member)
  52. member.members.filter_map do |m|
  53. MethodDoc.new(m.name) if m.method? || m.property?
  54. end
  55. end
  56. end
  57. class ClassDoc
  58. def initialize(name, methods)
  59. @name = name
  60. @methods = methods
  61. end
  62. attr_reader :name, :methods
  63. end
  64. class MethodDoc
  65. def initialize(name)
  66. @name = name
  67. end
  68. attr_reader :name
  69. end
  70. require 'dry/inflector'
  71. class RubyMethodName
  72. def initialize(js_name)
  73. @js_name = js_name
  74. @inflector = Dry::Inflector.new do |inflection|
  75. inflection.acronym('JavaScript', 'XPath')
  76. end
  77. end
  78. NAME_MAP = {
  79. '$' => :query_selector,
  80. '$$' => :query_selector_all,
  81. '$eval' => :eval_on_selector,
  82. '$$eval' => :eval_on_selector_all,
  83. '$x' => :Sx,
  84. 'type' => :type_text,
  85. 'getProperty' => :[],
  86. }.freeze
  87. def candidates
  88. Enumerator.new do |result|
  89. if NAME_MAP[@js_name]
  90. result << NAME_MAP[@js_name]
  91. end
  92. if snake_cased_name.start_with?("set_") # FIXME: check if with single arg.
  93. result << "#{snake_cased_name[4..-1]}=".to_sym
  94. elsif snake_cased_name.start_with?("get_")
  95. result << snake_cased_name[4..-1].to_sym
  96. elsif snake_cased_name.start_with?("is_") # FIXME: check if returns boolean
  97. result << "#{snake_cased_name[3..-1]}?".to_sym
  98. end
  99. result << snake_cased_name.to_sym
  100. result << "async_#{snake_cased_name}".to_sym
  101. end
  102. end
  103. private def snake_cased_name
  104. @snake_cased_name ||= @inflector.underscore(@js_name)
  105. end
  106. end
  107. class ImplementedClassPresenter
  108. def initialize(impl, doc)
  109. @class = impl
  110. @doc = doc
  111. end
  112. def api_coverages
  113. Enumerator.new do |data|
  114. data << ''
  115. data << "## #{class_name}"
  116. data << ''
  117. methods.each do |presenter|
  118. presenter.api_coverages.each(&data)
  119. end
  120. end
  121. end
  122. private def class_name
  123. @doc.name
  124. end
  125. private def methods
  126. @doc.methods.map do |method_doc|
  127. ruby_method_name = RubyMethodName.new(method_doc.name).candidates.find do |candidate|
  128. @class.public_instance_methods.include?(candidate)
  129. end
  130. if ruby_method_name
  131. impl = @class.public_instance_method(ruby_method_name)
  132. ImplementedMethodPresenter.new(impl, method_doc)
  133. else
  134. UnimplementedMethodPresenter.new(method_doc)
  135. end
  136. end
  137. end
  138. end
  139. class UnimplementedClassPresenter
  140. def initialize(doc)
  141. @doc = doc
  142. end
  143. def api_coverages
  144. Enumerator.new do |data|
  145. data << ''
  146. data << "## ~~#{class_name}~~"
  147. data << ''
  148. methods.each do |presenter|
  149. presenter.api_coverages.each(&data)
  150. end
  151. end
  152. end
  153. private def class_name
  154. @doc.name
  155. end
  156. private def methods
  157. @doc.methods.map do |method_doc|
  158. UnimplementedMethodPresenter.new(method_doc)
  159. end
  160. end
  161. end
  162. class ImplementedMethodPresenter
  163. def initialize(impl, doc)
  164. @method = impl
  165. @doc = doc
  166. end
  167. def api_coverages
  168. Enumerator.new do |data|
  169. data << method_line
  170. end
  171. end
  172. private def method_line
  173. if @doc.name == @method.name.to_s
  174. "* #{@doc.name}"
  175. else
  176. "* #{@doc.name} => `##{@method.name}`"
  177. end
  178. end
  179. end
  180. class UnimplementedMethodPresenter
  181. def initialize(doc)
  182. @doc = doc
  183. end
  184. def api_coverages
  185. Enumerator.new do |data|
  186. data << "* ~~#{@doc.name}~~"
  187. end
  188. end
  189. end
  190. apiversion_content = File.read(File.join(__dir__, 'DOCS_VERSION')).strip
  191. apidoc_content = File.read(File.join(__dir__, 'puppeteer.api.json'))
  192. parser = ApiDocJsonParser.new(apidoc_content)
  193. class_docs = parser.class_docs
  194. class_docs.delete_if { |doc| doc.name.start_with?('Puppeteer') }
  195. class_docs.unshift(parser.puppeteer_doc)
  196. File.open(File.join('.', 'docs', 'api_coverage.md'), 'w') do |f|
  197. f.write("# API coverages\n")
  198. f.write("- Puppeteer version: v#{apiversion_content}\n")
  199. f.write("- puppeteer-ruby version: #{Puppeteer::VERSION}\n")
  200. end
  201. require 'puppeteer'
  202. class_docs.each do |class_doc|
  203. klass =
  204. if Puppeteer.const_defined?(class_doc.name)
  205. impl = Puppeteer.const_get(class_doc.name)
  206. ImplementedClassPresenter.new(impl, class_doc)
  207. else
  208. UnimplementedClassPresenter.new(class_doc)
  209. end
  210. File.open(File.join('.', 'docs', 'api_coverage.md'), 'a') do |f|
  211. klass.api_coverages.each do |line|
  212. f.write(line)
  213. f.write("\n")
  214. end
  215. end
  216. end