api_doc.rb
1 ## ApiDoc 2 ## 3 # This is a utility for generating a markdown document that can be consumed by the 4 # Slate api publishing tool. ApiDoc.generate_markdown_for_slate will output a 5 # slate-ready markdown file to docs/slate/source/index.html.md. It works by building 6 # a collection of ApiDoc::Example objects for each endpoint in the ArchivesSpace 7 # backend, and then emitting them into the markdown document. There are three ways to 8 # create an example: 9 # 1) by editing files in `backend/controllers` and supplying handwritten examples. 10 # 11 # 2) by registering examples using this class. Example: 12 # ApiDoc.register_example "/search", :get, { 13 # q: "important papers", 14 # aq: build(:json_advanced_query), 15 # page: 1, 16 # page_size: 10 17 # }, "basic search with 10 results per page" 18 # 19 # 3) by doing nothing and allowing ApiDoc to build examples using FactoryBot 20 # factories defined in `backend/spec/factories.rb` 21 22 require 'factory_bot' 23 require 'uri' 24 require 'rack/test' 25 require 'jsonmodel' 26 require 'erb' 27 require_relative "../common/jsonmodel_translatable.rb" 28 require_relative '../backend/spec/spec_helper.rb' 29 30 class ArchivesSpaceService 31 def current_user 32 User.first 33 end 34 35 def high_priority_request? 36 false 37 end 38 end 39 40 BACKEND_URL = ENV['ASPACE_BACKEND_URL'] || "http://localhost:8089" 41 42 FactoryBot.define do 43 to_create { |instance| 44 instance.uri = instance.class.uri_for(99, repo_id: 99) 45 } 46 end 47 48 require_relative '../backend/spec/factories.rb' 49 50 class ApiDoc 51 include FactoryBot::Syntax::Methods 52 53 @@shell_example_erb = ERB.new(File.read(File.join(File.dirname(__FILE__), 'shell_example.erb')), nil, '-') 54 @@examples= {} 55 56 def self.generate_markdown_for_slate 57 slate_erb = ERB.new(File.read(File.join(File.dirname(__FILE__), 'API.erb')), nil, '<>') 58 slate_md = File.join(File.dirname(__FILE__), 'slate', 'source', 'index.html.md') 59 endpoints = ArchivesSpaceService::Endpoint.all.sort{|a,b| a[:uri] <=> b[:uri]}.reject { |ep| ep[:uri] == '/slug' } 60 61 endpoints.each do |endpoint| 62 next if endpoint[:examples]['shell'] 63 endpoint[:examples]['shell'] = ApiDoc.shell_examples_for endpoint 64 end 65 66 admin_auth_response_body = JSON.pretty_generate({ session: "9528190655b979f00817a5d38f9daf07d1686fed99a1d53dd2c9ff2d852a0c6e", 67 user: JSON.parse( 68 ( JSONModel::HTTP.get_response JSONModel::JSONModel(:user).my_url(1) ).body ) }) 69 70 File.open(slate_md, 'w') do |f| 71 f.write slate_erb.result(binding) 72 end 73 end 74 75 # alternative to letting examples auto-generate is to register see scripts/tasks/docs.thor 76 def self.register_example(uri, method, params, comment) 77 @@examples[uri] ||= {} 78 @@examples[uri][method] ||= [] 79 @@examples[uri][method] << Example.new(uri, method, params, comment) 80 end 81 82 def self.collect_examples(endpoint, method) 83 if @@examples[endpoint[:uri]] && @@examples[endpoint[:uri]][method] 84 @@examples[endpoint[:uri]][method] unless @@examples[endpoint[:uri]][method].empty? 85 elsif endpoint[:paginated] 86 variations = [ 87 [:page, 1, "return first 10 records"], 88 [:all_ids, true, "return an array of all the ids"], 89 [:id_set, '1,2,3,5,8', "return first 5 records in the Fibonacci sequence"] 90 ].map do |pagination_option| 91 (key, value, comment) = pagination_option 92 example = Example.from_endpoint_defn(endpoint, method) 93 example.add_param(key, value) 94 example.comment = comment 95 example 96 end 97 else 98 [Example.from_endpoint_defn(endpoint, method)] 99 end 100 end 101 102 def self.shell_examples_for(endpoint) 103 # look up if already registered; otherwise generate from endpoint 104 examples = endpoint[:method].map { |method| collect_examples(endpoint, method) }.flatten.compact 105 @@shell_example_erb.result(binding) 106 end 107 108 class Example 109 attr_accessor :comment 110 attr_reader :params 111 112 @@json_examples = {} 113 114 JSONModel.models.each_pair do |type, klass| 115 next if type =~ /^abstract_/ 116 @@json_examples[type] = JSON.parse( FactoryBot.build("json_#{type}".to_sym).to_json ) 117 end 118 119 @@json_examples.freeze 120 121 def self.from_endpoint_defn(endpoint, method) 122 raise "#{method} not in #{endpoint[:method]}" unless endpoint[:method].include? method 123 params = {} 124 uri = endpoint[:uri].dup 125 endpoint[:params].each do |param_defn| 126 raise param_defn unless param_defn.is_a?(Array) 127 (name, type, doc, opts) = param_defn 128 param_class = type.is_a?(Array) ? type[0] : type 129 value = if name == "resolve" && endpoint[:returns][0][1].match(/\(:[a-z]+\)/) 130 type = eval(endpoint[:returns][0][1]) 131 type = type[0] if type.is_a? Array 132 schema = JSONModel::JSONModel(type).schema 133 schema["properties"].select {|k, v| v['type'] == 'object' && v['subtype'] == 'ref'}.keys 134 elsif param_class.is_a?(Symbol) 135 param_class.to_s 136 elsif param_class.respond_to?(:record_type) 137 raise "Missing Factory #{param_class.record_type}" unless @@json_examples[param_class.record_type] 138 @@json_examples[param_class.record_type] 139 elsif param_class.to_s.include?("RESTHelpers") 140 param_class.to_s.split("::").last 141 elsif param_class == Integer 142 "1" 143 elsif param_class == String 144 "" 145 elsif param_class == Username 146 "example_username" 147 else 148 $stderr.puts param_class 149 raise "No param value for #{param_defn} in endpoint #{endpoint[:uri]}" 150 end 151 if opts && opts[:body] && value.is_a?(Hash) 152 params.merge!(value) 153 elsif type.is_a?(Array) || value.is_a?(Array) 154 params["#{name}[]"] = value 155 else 156 params[name] = value 157 end 158 end 159 params.each do |k, v| 160 params.delete(k) if uri.sub!(":#{k}", v) if v.is_a?(String) 161 end 162 # in practice only .xml formats used for :fmt params: 163 uri.sub!(":fmt", "xml") 164 params = {} if endpoint[:no_data] 165 self.new(uri, method, params, "auto-generated example") 166 end 167 168 def self.prune_params(params) 169 pruned = params.dup 170 pruned.each do |k, v| 171 if v.is_a? Hash 172 pruned[k] = prune_params(v) 173 end 174 end 175 pruned.reject! { |k,v| v.respond_to?(:empty?) && v.empty? } 176 pruned 177 end 178 179 def initialize(uri, method, params, comment = nil) 180 @method = method.to_sym 181 uri.sub!(":repo_id", (params[:repo_id] || 2).to_s) 182 uri.sub!(":id", (params[:id] || 1).to_s) 183 @uri = BACKEND_URL + uri.gsub(":repo_id", "2").gsub(":id", "1") 184 @params = params 185 @comment = comment 186 end 187 188 def get? 189 @method == :get 190 end 191 192 def post? 193 @method == :post 194 end 195 196 def delete? 197 @method == :delete 198 end 199 200 def data? 201 @params && !@params.keys.empty? 202 end 203 204 def form_payload 205 pruned = self.class.prune_params(@params) 206 JSON.pretty_generate(pruned) 207 end 208 209 def has_param?(param) 210 @params.keys.map { |k| k.to_s }.include?(param.to_s) 211 end 212 213 def add_param(key, value) 214 raise "already has #{key}" if has_param? key 215 @params[key] = value 216 end 217 218 def request_url 219 if get? && data? 220 "#{@uri}?#{URI.encode_www_form(@params)}" 221 else 222 @uri 223 end 224 end 225 226 end 227 end 228 229 include FactoryBot::Syntax::Methods 230 231 232 # Use the test factories, or register a bespoke example, or provide 233 # an example manually withing the backend/app/controller/*.rb files 234 ApiDoc.register_example "/search", :get, { 235 q: "important papers", 236 aq: build(:json_advanced_query), 237 page: 1, 238 page_size: 10 239 }, "basic search with 10 results per page" 240 241 ApiDoc.register_example "/repositories/:repo_id/top_containers/bulk/barcodes", :post, { 242 repo_id: 1, 243 barcode_data: "{\"/repositories/:repo_id/top_containers/1\":\"8675309\"}" 244 }, "assign barcodes in bulk by posting a hash"