require 'ostruct' module Documentor def service_overview(rules) docs = documentation(rules) docs = docs || ''; docs = docs.gsub(/.+?<\/fullname>/, '') docs = docs.gsub(/(.*?)\{(\S*?)\}/, '\1[\2]') ## replace {} in format string with [] if docs && (docs.include? 'runtime aspects of your deployed APIs') puts docs end docs == '' ? nil : docs end def documentation(rules) docs = rules['documentation'] || '' docs = docs.gsub(//m, '') docs = docs.gsub(/.+?<\/examples?>/m, '') docs = docs.gsub(/\s*<\/note>/m, '') docs = docs.gsub(/(.+?)<\/note>/m) do text = $1.gsub(/<\/?p>/, '') "
Note: #{text}
" end docs = docs.gsub(/\{(\S+)\}/, '`{\1}`') docs = docs.gsub(/\s+/, ' ').strip ## The markdown to html converter incorrectly replaces underscores and asterisks in 'code' tags with 'em' tags. ## html escape these symbols to get around this. docs = docs.gsub(/(.+?)<\/code>/m) do ## strip out extraneous code blocks text = $1.gsub('`','') text = text.gsub('_', '_') text = text.gsub('*', '*') "#{text}" end docs == '' ? nil : docs end def method_name(name, downcased = true) name = name.sub(/\d{4}_\d{2}_\d{2}$/, '') downcased ? name[0].downcase + name[1..-1] : name end end class ModelDocumentor include Documentor attr_reader :lines def initialize(klass, api) api_version = api['metadata']['apiVersion'] service_desc = service_overview(api) @lines = [] @lines << '' @lines << <<-DOCS.strip Constructs a service interface object. Each API operation is exposed as a function on service. DOCS if service_desc @lines << <<-DOCS.strip ### Service Description #{service_desc} DOCS end @lines << <<-DOCS.strip ### Sending a Request Using #{klass} ```javascript var #{klass.downcase} = new AWS.#{klass}(); #{klass.downcase}.#{find_example_operation(api)}(params, function (err, data) { if (err) console.log(err, err.stack); // an error occurred else console.log(data); // successful response }); ``` ### Locking the API Version In order to ensure that the #{klass} object uses this specific API, you can construct the object by passing the `apiVersion` option to the constructor: ```javascript var #{klass.downcase} = new AWS.#{klass}({apiVersion: '#{api_version}'}); ``` You can also set the API version globally in `AWS.config.apiVersions` using the **#{klass.downcase}** service identifier: ```javascript AWS.config.apiVersions = { #{klass.downcase}: '#{api_version}', // other service API versions }; var #{klass.downcase} = new AWS.#{klass}(); ``` DOCS end def find_example_operation(api) list = api['operations'].keys.grep(/describe|list|get/) list = api['operations'].keys if list.size == 0 method_name(list.first) end end class MethodDocumentor include Documentor attr_reader :lines def event_stream_output(operation, api) @eventStreamOutputShape = nil @eventStreamOutputName = nil output = operation['output'] if output && output['shape'] outputShapeName = output['shape'] outputShape = api['shapes'][outputShapeName] if outputShape['eventstream'] @eventStreamOutputShape = outputShapeName @eventStreamOutputName = output else outputShape['members'].each_pair do |memberName, member| if api['shapes'][member['shape']]['eventstream'] @eventStreamOutputShape = member['shape'] @eventStreamOutputName = memberName end end end end end def node_event_stream_output_examples(klass, operation_name, operation, api) lines = [] if @eventStreamOutputShape lines << "@example Node.js EventStream Example" lines << " // In Node.js, events are streamed and can be read as they arrive." lines << " #{klass.downcase}.#{method_name(operation_name)}({/** params **/}, function(err, data) {" lines << " if (err) {" lines << " // handle error" lines << " return console.error(err);" lines << " }" lines << "" lines << " var eventStream = data.#{@eventStreamOutputName};" lines << "" lines << " eventStream.on('data', function(event) {" lines << " // Check the top-level field to determine which event this is." memberNum = 0 members = api['shapes'][@eventStreamOutputShape]['members'] members.each_pair do |memberName, member| prefix = "" if memberNum > 0 prefix = "} else " end lines << " #{prefix}if (event.#{memberName}) {" lines << " // handle #{memberName} event" if (memberNum == members.count - 1) lines << " }" end memberNum = memberNum + 1 end lines << " });" lines << " eventStream.on('error', function(err) { /** Handle error events **/});" lines << " eventStream.on('end', function() { /** Finished reading all events **/});" lines << " });" lines << "" end lines end def browser_event_stream_output_examples(klass, operation_name, operation, api) lines = [] if @eventStreamOutputShape lines << "@example Browser EventStream Example" lines << " // In browsers, events aren't processed until the response is fully buffered." lines << " // Events will be accessible as an array." lines << " #{klass.downcase}.#{method_name(operation_name)}({/** params **/}, function(err, data) {" lines << " if (err) {" lines << " // handle error" lines << " return console.error(err);" lines << " }" lines << "" lines << " var events = data.#{@eventStreamOutputName};" lines << "" lines << " for (var event of events) {" lines << " // Check the top-level field to determine which event this is." memberNum = 0 members = api['shapes'][@eventStreamOutputShape]['members'] members.each_pair do |memberName, member| prefix = "" if memberNum > 0 prefix = "} else " end lines << " #{prefix}if (event.#{memberName}) {" lines << " // handle #{memberName} event" if (memberNum == members.count - 1) lines << " }" end memberNum = memberNum + 1 end lines << " }" lines << " });" lines << "" end lines end def async_event_stream_output_examples(klass, operation_name, operation, api) lines = [] if @eventStreamOutputShape lines << "@example Async Iterator EventStream Example (Experimental)" lines << " // In Node.js v10.x, Readable streams have experimental support for async iteration." lines << " // Instead of listening to the event stream's 'data' event, you can use a for...await loop." lines << " async function example() {" lines << " try {" lines << " const result = await #{klass.downcase}.#{method_name(operation_name)}({/** params **/}).promise();" lines << "" lines << " const events = result.#{@eventStreamOutputName};" lines << "" lines << " for await (const event of events) {" lines << " // Check the top-level field to determine which event this is." memberNum = 0 members = api['shapes'][@eventStreamOutputShape]['members'] members.each_pair do |memberName, member| prefix = "" if memberNum > 0 prefix = "} else " end lines << " #{prefix}if (event.#{memberName}) {" lines << " // handle #{memberName} event" if (memberNum == members.count - 1) lines << " }" end memberNum = memberNum + 1 end lines << " }" lines << " } catch (err) {" lines << " // handle error" lines << " }" lines << " }" lines << "" end lines end def event_stream_output_examples(klass, operation_name, operation, api) lines = [] lines << node_event_stream_output_examples(klass, operation_name, operation, api) lines << browser_event_stream_output_examples(klass, operation_name, operation, api) lines << async_event_stream_output_examples(klass, operation_name, operation, api) lines end def initialize(operation_name, operation, api, klass, options = {}, examples = {}) desc = documentation(operation) desc ||= "Calls the #{method_name(operation_name, false)} API operation." desc = desc.gsub(/^\s+/, '').strip if klass === 'S3' && (['CreateBucket', 'ListBuckets', 'DeleteBucket'].include? operation_name) desc = desc + '

Note: This operation cannot be used in a browser. S3 does not support CORS on this operation.

' end event_stream_output(operation, api) if options[:flatten_dynamodb_attrs] desc = "" end @lines = [desc, ''] ## @param tags @lines << "@param params [Object]" @lines += shapes(api, operation['input'], {:in_output => false}.merge(options)).map {|line| " " + line } if examples examples.each do |example| begin sharedExample = generate_shared_example(api, example, klass, method_name(operation_name)).split("\n").map {|line| " " + line} @lines << "@example #{example['title']}" @lines << "" @lines << " /* #{example['description']} */" @lines << "" @lines << sharedExample rescue => exception puts "[warn]: Error encountered generating example for #{klass}.#{operation_name}: #{exception}" end end end if @eventStreamOutputShape @lines << event_stream_output_examples(klass, operation_name, operation, api) end ## @example tag @lines << "@example Calling the #{method_name(operation_name)} operation" @lines << generate_example(api, klass, method_name(operation_name), operation['input'], options).split("\n").map {|line| " " + line } @lines << "" ## @callback tag @lines << "@callback callback function(err, data)" @lines << " Called when a response from the service is returned. If a" @lines << " callback is not supplied, you must call {AWS.Request.send}" @lines << " on the returned request object to initiate the request." @lines << " @context [AWS.Response] the response object containing error, " + "data properties, and the original request object." @lines << " @param err [Error] the error object returned from the request." @lines << " Set to `null` if the request is successful." @lines << " @param data [Object] the de-serialized data returned from" @lines << " the request. Set to `null` if a request error occurs." output = shapes(api, operation['output'], {:in_output => true}.merge(options)) output = output.map {|line| " " + line } if output.size > 0 @lines << " The `data` object has the following properties:" @lines << "" @lines += output end ## @return tag @lines << "@return [AWS.Request] a handle to the operation request for" @lines << " subsequent event callback registration." if operation['documentation_url'] @lines << "@see #{operation['documentation_url']}" @lines << " #{api['serviceAbbreviation']} Documentation for #{operation_name}" end ## Service Reference if api['metadata']['uid'] @lines << '
' @lines << '

Service Reference:

' @lines << '' @lines << '
' end end def shapes(api, rules, options = {}) rules = api['shapes'][rules['shape']] if rules && rules['shape'] if rules and rules['members'] rules['members'].map do |name, member_rules| if member_rules['shape'] member_rules = api['shapes'][member_rules['shape']].merge(member_rules) end opts = {:name => name}.merge(options) ShapeDocumentor.new(api, member_rules, opts).lines end.flatten else [] end end def generate_example(api, klass, name, input, options = {}) ExampleShapeVisitor.new(api, options).example(klass, name, input) end def generate_shared_example(api, example, klass, name) SharedExampleVisitor.new(api, example, klass, name).example end end class SharedExampleVisitor def initialize(api, example, klass, name) @api = api @example = example @klass = klass @name = name @comments = example['comments'] end def shape_type(type) case type when 'structure' then 'StructureShape' when 'list' then 'ListShape' when 'map' then 'MapShape' when 'boolean' then 'BooleanShape' when 'timestamp' then 'TimestampShape' when 'float', 'double', 'bigdecimal' then 'FloatShape' when 'integer', 'long', 'short', 'biginteger' then 'IntegerShape' when 'string', 'character' then 'StringShape' when 'binary', 'blob' then 'BinaryShape' else type end end def example operation = @name[0].upcase + @name[1..-1] operation_input = @api['operations'][operation]['input'] if operation_input input_shape_name = operation_input['shape'] input_shape = @api['shapes'][input_shape_name] input = visit(input_shape, @example['input'] || {}, "", [], @comments['input']) else input = "{}" end lines = ["var params = #{input};"] lines << "#{@klass.downcase}.#{@name}(params, function(err, data) {" lines << " if (err) console.log(err, err.stack); // an error occurred" lines << " else console.log(data); // successful response" operation_output = @api['operations'][operation]['output'] if operation_output output_shape_name = operation_output['shape'] output_shape = @api['shapes'][output_shape_name] if output = visit(output_shape, @example['output'] || {}, " ", [], @comments['output']) lines << " /*" lines << " data = #{output}" lines << " */" end end lines << "});" lines.join("\n") end def visit(shape, value, indent, path, comments) case shape_type(shape['type']) when 'StructureShape' then structure(shape, value, indent, path, comments) when 'MapShape' then map(shape, value, indent, path, comments) when 'ListShape' then list(shape, value, indent, path, comments) when 'StringShape' then value.inspect when 'TimestampShape' then "" when 'BinaryShape' then "" else value end end def structure(shape, value, indent, path, comments) lines = ["{"] value_length = value.length value.each_with_index do |(key, val), index| path << ".#{key}" comment = apply_comment(path, comments) shape_name = shape['members'][key]['shape'] shape_val = visit(@api['shapes'][shape_name], val, "#{indent} ", path, comments) if index < value_length - 1 then comment = ", " + comment end lines << "#{indent} #{key}: #{shape_val}#{comment}" path.pop end lines << "#{indent}}" lines.join("\n") end def map(shape, value, indent, path, comments) lines = ["{"] value_length = value.length value.each_with_index do |(key, val), index| path << ".#{key}" comment = apply_comment(path, comments) shape_name = shape['value']['shape'] shape_val = visit(@api['shapes'][shape_name], val, "#{indent} ", path, comments) if index < value_length - 1 then comment = ", " + comment end lines << "#{indent} \"#{key}\": #{shape_val}#{comment}" path.pop end lines << "#{indent}}" lines.join("\n") end def list(shape, value, indent, path, comments) lines = ["["] value_length = value.length value.each_with_index do |member, index| path << "[#{index}]" comment = apply_comment(path, comments) shape_name = shape['member']['shape'] shape_val = visit(@api['shapes'][shape_name], member, "#{indent} ", path, comments) if index < value_length - 1 then comment = ", " + comment end lines << "#{indent} #{shape_val}#{comment}" path.pop end lines << "#{indent}]" lines.join("\n") end def apply_comment(path, comments) key = path.join().sub(/^\./, '') if comments && comments[key] "// #{comments[key]}" else "" end end end class ExampleShapeVisitor def initialize(api, options = {}) @api = api @required_only = options[:required_only] || false @flatten_dynamodb_attrs = options[:flatten_dynamodb_attrs] @visited = Hash.new { 0 } @recursive = {} end def example(klass, name, input) lines = [] params = traverse(input) params_var = "" if params.strip.length > 0 lines << "var params = " + params + ";" params_var = "params, " end lines << "#{klass.downcase}.#{name}(#{params_var}function(err, data) {" lines << " if (err) console.log(err, err.stack); // an error occurred" lines << " else console.log(data); // successful response" lines << "});" lines.join("\n") end def traverse(node, required = false) return "" if node.nil? result = "" @visited[node['shape']] += 1 if @flatten_dynamodb_attrs && node['shape'] == "AttributeValue" result = 'someValue /* "str" | 10 | true | false | null | [1, "a"] | {a: "b"} */' elsif !node['shape'] || @visited[node['shape']] < 2 node = @api['shapes'][node['shape']].merge(node) if node['shape'] if (meth = "visit_" + (node['type'] || 'string')) && respond_to?(meth) result = send(meth, node, required) end else @recursive[node['shape']] = true result = "/* recursive #{node['shape']} */" end @visited[node['shape']] -= 1 result end def visit_structure(node, required = false) required_map = (node['required'] || []).inject({}) {|h,k| h[k] = true; h } datas = node['members'].sort_by {|n, v| [required_map[n] ? -1 : 1, n] }.map do |key, value| next if @required_only && !required_map[key] value = @api['shapes'][value['shape']].merge(value) if value['shape'] [key, value, indent(traverse(value, required_map[key]), false)] end.compact lines = ["{" + mark_rec_shape(node) + (required ? " /* required */" : "")] datas.each_with_index do |(key, value, data), index| lines << " #{key}: " + data + (index + 1 < node['members'].size ? "," : "") + (required_map[key] && !%w(list map structure).include?(value['type']) ? " /* required */" : "") end lines << "}" lines.join("\n") end def visit_list(node, required = false) data = indent(traverse(node['member'])) lines = ["[" + mark_rec_shape(node) + (required ? " /* required */" : "")] lines << data + "," lines << " /* more items */" lines << "]" lines.join("\n") end def visit_map(node, required = false) data = indent("'<#{node['key']['shape']}>': " + traverse(node['value'])) lines = ["{" + mark_rec_shape(node) + (required ? " /* required */" : "")] lines << data + "," lines << " /* '<#{node['key']['shape']}>': ... */" lines << "}" lines.join("\n") end def visit_string(node, required = false) if node['jsonvalue'] "any /* This value will be JSON encoded on your behalf with JSON.stringify() */" elsif node['enum'] node['enum'].join(' | ') else "'STRING_VALUE'" end end def visit_integer(node, required = false) "'NUMBER_VALUE'" end alias visit_long visit_integer def visit_float(node, required = false) "'NUMBER_VALUE'" end alias visit_double visit_float alias visit_bigdecimal visit_float def visit_boolean(node, required = false) "true || false" end def visit_binary(node, required = false) value = "Buffer.from('...') || 'STRING_VALUE'" if node['streaming'] value += " || streamObject" else value += " /* Strings will be Base-64 encoded on your behalf */" end value end alias visit_blob visit_binary def visit_timestamp(node, required = false) "new Date || 'Wed Dec 31 1969 16:00:00 GMT-0800 (PST)' || 123456789" end def indent(text, first_line = true, n = 2) text = text.split(/\r?\n/).map {|l| "#{' ' * n}#{l}" }.join("\n") text = text.sub(/\A\s+/, '') if !first_line text end def mark_rec_shape(node) node['shape'] && @recursive[node['shape']] ? " /* #{node['shape']} */" : "" end end class ShapeDocumentor include Documentor attr_reader :name attr_reader :rules attr_reader :lines attr_reader :nested_lines attr_reader :prefix attr_reader :type def self.type_for(rules, options={}) type = rules['type'] normalizedType = type if type == 'structure' if rules['eventstream'] normalizedType = 'ReadableStream | Array' else normalizedType = 'map' end elsif type == 'list' normalizedType = 'Array' elsif type == 'map' normalizedType = 'map' elsif type == 'string' || type == nil normalizedType = 'String' elsif type == 'integer' normalizedType = 'Integer' elsif type == 'long' normalizedType = 'Integer' elsif type == 'float' normalizedType = 'Float' elsif type == 'double' normalizedType = 'Float' elsif type == 'bigdecimal' normalizedType = 'Float' elsif type == 'boolean' normalizedType = 'Boolean' elsif type == 'binary' || type == 'blob' normalizedType = "Buffer" unless rules['eventpayload'] if !!options[:in_output] normalizedType += "(Node.js), Typed Array(Browser)" else normalizedType += ", Typed Array, Blob, String" end end elsif type == 'timestamp' normalizedType = 'Date' else raise "unhandled type: #{rules['type']}" end # TODO : update this format description once we add streaming uploads normalizedType += ', ReadableStream' if rules['streaming'] normalizedType end def initialize(api, rules, options = {}) rules = api['shapes'][rules['shape']].merge(rules) if rules['shape'] @api = api @rules = rules @name = options[:name] @prefix = options[:prefix] || '' @required = !!options[:required] @visited = options[:visited] || Hash.new { 0 } @type = self.class.type_for(rules, options) @isEventStream = rules['eventstream'] @lines = [] @nested_lines = [] @flatten_dynamodb_attrs = options[:flatten_dynamodb_attrs] if @flatten_dynamodb_attrs && rules['shape'] == "AttributeValue" desc = <<-doc_client.gsub(/\n/, '').strip — a serializable JavaScript object. For information about the supported types see the [DynamoDB Data Model](http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DataModel.html) doc_client @lines = ["#{prefix}* `#{name}` #{desc}"] @nested_lines += [desc] return end if structure? required_map = (rules['required'] || []).inject({}) {|h,k| h[k] = true; h } rules['members'].each_pair do |name, member_rules| @nested_lines += child_shape(member_rules, :name => name, :required => required_map[name]).lines end end if list? child = child_shape(rules['member'] || {}, :prefix => prefix) @type += "<#{child.type}>" @nested_lines += child.nested_lines end if map? # sanity check, I don't think this should ever raise, but if it # does we will have to document the key shape #key_child = child_shape(rules['key'] || {}, :prefix => prefix) #raise "unhandled map key type" if key_child.type != 'String' child = child_shape(rules['value'] || {}, :prefix => prefix) @type += "<#{child.type}>" @nested_lines += child.nested_lines end @lines = ["#{prefix}* `#{name}` #{description}"] if @isEventStream @lines << "#{name} is an object-mode Readable stream in Node.js v0.10.x and higher. Attach a listener to the `data` event to receive events." @lines << "#{name} is an array of events in browsers." @lines << "The possible events that may be returned are listed below. Note that the top-level field in each event matches the event name." end @lines += @nested_lines if rules['enum'] @lines << "#{prefix} Possible values include:" @lines += rules['enum'].map{|v| "#{prefix} * `#{v.inspect}`" } end if rules['idempotencyToken'] @lines << "#{prefix} If a token is not provided, the SDK will use a version 4 UUID." end end def structure? rules['type'] == 'structure' end def list? rules['type'] == 'list' end def map? rules['type'] == 'map' end def description text = [] if @required text << "— **required** — (`#{@type}`)" else text << "— (`#{@type}`)" end if (docs = documentation(rules)) && !@flatten_dynamodb_attrs text << " #{docs}" end text.join(' ') end private def child_shape(rules, options = {}) @visited[rules['shape']] += 1 if @visited[rules['shape']] < 2 ret = ShapeDocumentor.new(@api, rules, { :prefix => prefix + ' ', :visited => @visited, :flatten_dynamodb_attrs => @flatten_dynamodb_attrs }.merge(options)) else ret = OpenStruct.new({ :nested_lines => [], :lines => [], :type => @api['shapes'] ? ShapeDocumentor.type_for(@api['shapes'][rules['shape']]) : rules['shape'] }.merge(options)) end @visited[rules['shape']] -= 1 ret end end