/ docs / api_doc.rb
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"