api_sketch 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.ruby-gemset +1 -0
  4. data/.ruby-version +1 -0
  5. data/CHANGELOG.md +8 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +404 -0
  9. data/Rakefile +8 -0
  10. data/api_sketch.gemspec +30 -0
  11. data/bin/api_sketch +10 -0
  12. data/lib/api_sketch/config.rb +7 -0
  13. data/lib/api_sketch/dsl.rb +213 -0
  14. data/lib/api_sketch/error.rb +2 -0
  15. data/lib/api_sketch/examples_server.rb +54 -0
  16. data/lib/api_sketch/generators.rb +87 -0
  17. data/lib/api_sketch/helpers.rb +21 -0
  18. data/lib/api_sketch/model.rb +227 -0
  19. data/lib/api_sketch/renderers.rb +48 -0
  20. data/lib/api_sketch/runner.rb +92 -0
  21. data/lib/api_sketch/templates/bootstrap/assets/fonts/glyphicons-halflings-regular.eot +0 -0
  22. data/lib/api_sketch/templates/bootstrap/assets/fonts/glyphicons-halflings-regular.svg +229 -0
  23. data/lib/api_sketch/templates/bootstrap/assets/fonts/glyphicons-halflings-regular.ttf +0 -0
  24. data/lib/api_sketch/templates/bootstrap/assets/fonts/glyphicons-halflings-regular.woff +0 -0
  25. data/lib/api_sketch/templates/bootstrap/assets/images/favicon.ico +0 -0
  26. data/lib/api_sketch/templates/bootstrap/assets/javascripts/bootstrap.js +2114 -0
  27. data/lib/api_sketch/templates/bootstrap/assets/javascripts/bootstrap.min.js +6 -0
  28. data/lib/api_sketch/templates/bootstrap/assets/javascripts/docs.min.js +24 -0
  29. data/lib/api_sketch/templates/bootstrap/assets/javascripts/ie10-viewport-bug-workaround.js +22 -0
  30. data/lib/api_sketch/templates/bootstrap/assets/javascripts/jquery-1.11.1.min.js +4 -0
  31. data/lib/api_sketch/templates/bootstrap/assets/stylesheets/bootstrap-theme.css +442 -0
  32. data/lib/api_sketch/templates/bootstrap/assets/stylesheets/bootstrap-theme.css.map +1 -0
  33. data/lib/api_sketch/templates/bootstrap/assets/stylesheets/bootstrap-theme.min.css +5 -0
  34. data/lib/api_sketch/templates/bootstrap/assets/stylesheets/bootstrap.css +6203 -0
  35. data/lib/api_sketch/templates/bootstrap/assets/stylesheets/bootstrap.css.map +1 -0
  36. data/lib/api_sketch/templates/bootstrap/assets/stylesheets/bootstrap.min.css +5 -0
  37. data/lib/api_sketch/templates/bootstrap/assets/stylesheets/dashboard.css +130 -0
  38. data/lib/api_sketch/templates/bootstrap/resource.html.erb +215 -0
  39. data/lib/api_sketch/version.rb +3 -0
  40. data/lib/api_sketch.rb +19 -0
  41. data/spec/lib/dsl_spec.rb +474 -0
  42. data/spec/spec_helper.rb +1 -0
  43. metadata +187 -0
@@ -0,0 +1,213 @@
1
+ class ApiSketch::DSL
2
+
3
+ COMPLEX_ATTRIBUTE_NAMES = [:headers, :parameters, :responses]
4
+
5
+ def initialize(definitions_dir=ApiSketch::Config[:definitions_dir])
6
+ @definitions_dir = definitions_dir
7
+ end
8
+
9
+ def init!
10
+ Dir.glob("#{@definitions_dir}/**/*.rb").each do |file_path|
11
+ puts_info("\t read: #{file_path}")
12
+ binding.eval(File.open(File.expand_path(file_path)).read, file_path)
13
+ end
14
+ end
15
+
16
+ class AttributeParser
17
+
18
+ def initialize(container_type, &block)
19
+ @attribute_values = {}
20
+ @container_type = container_type
21
+ # INFO: Such long method name is used to ensure that we are would not have such value as key at hash
22
+ define_singleton_method(:set_attributes_as_hash_value_format, block)
23
+ set_attributes_as_hash_value_format
24
+ end
25
+
26
+ def method_missing(method_name, *arguments, &block)
27
+ @attribute_values[method_name] = arguments.first || block
28
+ end
29
+
30
+ def to_h
31
+ @attribute_values
32
+ end
33
+
34
+ end
35
+
36
+
37
+ class Attributes
38
+
39
+ TYPES = [:integer, :string, :float, :boolean, :datetime, :timestamp, :document, :array]
40
+
41
+ def initialize(container_type, &block)
42
+ @container_type = container_type
43
+ @params = []
44
+ define_singleton_method(:initialize_attributes, block)
45
+ initialize_attributes
46
+ end
47
+
48
+ def to_a
49
+ @params
50
+ end
51
+
52
+ def shared(name)
53
+ self.instance_eval(&::ApiSketch::Model::SharedBlock.find(name))
54
+ end
55
+
56
+ TYPES.each do |type_name|
57
+ define_method(type_name) do |*args, &block|
58
+ name = args.first
59
+ if @container_type == :document
60
+ if name.nil? || name.empty? # key name is not provided
61
+ raise ::ApiSketch::Error.new, "Key inside document should have name"
62
+ end
63
+ elsif @container_type == :array
64
+ if (!name.nil? && !name.empty?) # key name is provided
65
+ raise ::ApiSketch::Error.new, "Array element can't have name"
66
+ end
67
+ end
68
+ @params << self.class.build_by(type_name, name, &block)
69
+ end
70
+ end
71
+
72
+ class << self
73
+ def build_by(data_type, attribute_name, &block)
74
+ options = {data_type: data_type}
75
+ options[:name] = attribute_name if attribute_name
76
+ case data_type
77
+ when :document, :array
78
+ ::ApiSketch::Model::Attribute.new(::ApiSketch::DSL::ComplexAttributeParser.new(data_type, &block).to_h.merge(options))
79
+ else
80
+ ::ApiSketch::Model::Attribute.new(::ApiSketch::DSL::AttributeParser.new(data_type, &block).to_h.merge(options))
81
+ end
82
+ end
83
+ end
84
+
85
+ end
86
+
87
+ class ComplexAttributeParser < ApiSketch::DSL::AttributeParser
88
+
89
+ def method_missing(method_name, *arguments, &block)
90
+ if method_name == :content
91
+ @attribute_values[:content] = ApiSketch::DSL::Attributes.new(@container_type, &block).to_a
92
+ else
93
+ super(method_name, *arguments, &block)
94
+ end
95
+ end
96
+
97
+ end
98
+
99
+
100
+ class Headers
101
+
102
+ def initialize(&block)
103
+ @list = []
104
+ define_singleton_method(:initialize_headers_list, block)
105
+ initialize_headers_list
106
+ end
107
+
108
+ def to_a
109
+ @list
110
+ end
111
+
112
+ def add(name, &block)
113
+ @list << ::ApiSketch::Model::Header.new(::ApiSketch::DSL::AttributeParser.new(:document, &block).to_h.merge(name: name))
114
+ end
115
+
116
+ end
117
+
118
+ class Parameters
119
+
120
+ def initialize(&block)
121
+ @query = []
122
+ @body = []
123
+ @query_container_type = nil
124
+ @body_container_type = nil
125
+ define_singleton_method(:initialize_parameters_list, block)
126
+ initialize_parameters_list
127
+ end
128
+
129
+ def to_h
130
+ {
131
+ query: @query,
132
+ body: @body,
133
+ query_container_type: @query_container_type,
134
+ body_container_type: @body_container_type
135
+ }
136
+ end
137
+
138
+ def query(container_type, &block)
139
+ @query_container_type = container_type
140
+ @query += ::ApiSketch::DSL::Attributes.new(container_type, &block).to_a
141
+ end
142
+
143
+ def body(container_type, &block)
144
+ @body_container_type = container_type
145
+ @body += ::ApiSketch::DSL::Attributes.new(container_type, &block).to_a
146
+ end
147
+
148
+ end
149
+
150
+ class Responses
151
+
152
+ def initialize(&block)
153
+ @list = []
154
+ define_singleton_method(:initialize_responses_list, block)
155
+ initialize_responses_list
156
+ end
157
+
158
+ def to_a
159
+ @list
160
+ end
161
+
162
+ def context(name, &block)
163
+ attributes = ::ApiSketch::DSL::AttributeParser.new(:root, &block).to_h
164
+ if attributes[:parameters]
165
+ params = ::ApiSketch::DSL::Parameters.new(&attributes[:parameters]).to_h
166
+ attributes[:parameters] = ::ApiSketch::Model::Parameters.new(params)
167
+ end
168
+ @list << ::ApiSketch::Model::Response.new(attributes.merge(name: name))
169
+ end
170
+
171
+ end
172
+
173
+ def shared_block(name, block)
174
+ ::ApiSketch::Model::SharedBlock.add(name, block)
175
+ end
176
+
177
+ def resource(name, &block)
178
+ attributes = get_attrs(name, &block)
179
+
180
+ COMPLEX_ATTRIBUTE_NAMES.each do |attribute_name|
181
+ block_value = attributes[attribute_name]
182
+ attributes[attribute_name] = get_complex_attribute(attribute_name, &block_value) if block_value
183
+ end
184
+
185
+ # Assign resource namespace
186
+ attributes[:namespace] ||= block.source_location[0].gsub(definitions_dir, "").gsub(".rb", "").split("/").reject { |ns| ns.nil? || ns == "" }.join("/")
187
+
188
+ ::ApiSketch::Model::Resource.create(attributes)
189
+ end
190
+
191
+
192
+ private
193
+ def definitions_dir
194
+ @definitions_dir
195
+ end
196
+
197
+ def get_attrs(name, &block)
198
+ ::ApiSketch::DSL::AttributeParser.new(:root, &block).to_h.merge(name: name)
199
+ end
200
+
201
+ def get_complex_attribute(attribute_name, &block)
202
+ case attribute_name
203
+ when :headers
204
+ ::ApiSketch::DSL::Headers.new(&block).to_a
205
+ when :parameters
206
+ params = ::ApiSketch::DSL::Parameters.new(&block).to_h
207
+ ::ApiSketch::Model::Parameters.new(params)
208
+ when :responses
209
+ ::ApiSketch::DSL::Responses.new(&block).to_a
210
+ end
211
+ end
212
+
213
+ end
@@ -0,0 +1,2 @@
1
+ class ApiSketch::Error < StandardError
2
+ end
@@ -0,0 +1,54 @@
1
+ class ApiSketch::ExamplesServer
2
+ def self.call(env)
3
+ new(env).response.finish
4
+ end
5
+
6
+ def initialize(env)
7
+ @request = Rack::Request.new(env)
8
+ end
9
+
10
+ def response
11
+ if api_resource
12
+ api_response = if api_response_context
13
+ api_resource.responses.find { |rsp| rsp.name == api_response_context }
14
+ else
15
+ api_resource.responses.first
16
+ end
17
+
18
+ if api_response
19
+ Rack::Response.new do |response|
20
+ api_response.headers.each do |header|
21
+ response[header.name] = header.value
22
+ end
23
+
24
+ response['Content-Type'] = 'application/json'
25
+
26
+ response.status = Rack::Utils.status_code(api_response.http_status)
27
+ response.write(ApiSketch::ResponseRenderer.new(api_response.parameters.body, api_response.parameters.body_container_type).to_json)
28
+ end
29
+ else
30
+ api_sketch_message("No any responses defined for this resource", 404)
31
+ end
32
+ else
33
+ api_sketch_message("Resource is not Found", 404)
34
+ end
35
+ end
36
+
37
+ private
38
+ def api_resource
39
+ @api_resource = if @request.params["api_sketch_resource_id"]
40
+ ApiSketch::Model::Resource.find(@request.params["api_sketch_resource_id"])
41
+ else
42
+ ApiSketch::Model::Resource.all.find { |res| res.http_method == @request.request_method && res.path }
43
+ end
44
+ end
45
+
46
+ def api_response_context
47
+ @request.params["api_sketch_response_context"]
48
+ end
49
+
50
+ def api_sketch_message(message, status)
51
+ Rack::Response.new({"api_sketch" => message}.to_json, 404)
52
+ end
53
+
54
+ end
@@ -0,0 +1,87 @@
1
+ module ApiSketch::Generators
2
+
3
+ class Base
4
+
5
+ attr_accessor :definitions_dir, :documentation_dir
6
+
7
+ attr_reader :templates_folder
8
+
9
+ # TODO: Add here some validations for folders existance, etc
10
+ def initialize(options = {})
11
+ self.definitions_dir = options[:definitions_dir]
12
+ self.documentation_dir = options[:documentation_dir]
13
+ @templates_folder = File.expand_path("templates/#{self.class.name.split("::").last.downcase}", File.dirname(__FILE__))
14
+ end
15
+
16
+ def generate!
17
+ puts_info("Load definitions")
18
+ load_definitions
19
+ puts_info("Create documentation directory")
20
+ puts_info("\t path: #{self.documentation_dir}")
21
+ create_documentation_directory
22
+ puts_info("Create documentation files")
23
+ create_documentation_files
24
+ end
25
+
26
+ private
27
+ def create_documentation_directory
28
+ FileUtils.rm_r(self.documentation_dir, :force => true)
29
+ FileUtils.mkdir_p(self.documentation_dir)
30
+ end
31
+
32
+ # TODO: This is unfinished sample file generator it should be more complex at some other generators
33
+ # Other generors should inherit from this class and implement this method
34
+ def create_documentation_files
35
+ raise "This method should be implemented at child class who inherits from ApiSketch::Generators::Base"
36
+ end
37
+
38
+ def load_definitions
39
+ ApiSketch::Model::Resource.reload!(self.definitions_dir)
40
+ end
41
+
42
+ end
43
+
44
+
45
+ class Bootstrap < ApiSketch::Generators::Base
46
+
47
+ def initialize(options = {})
48
+ super(options)
49
+ @resource_template = ERB.new(File.read("#{self.templates_folder}/resource.html.erb"))
50
+ end
51
+
52
+ # This is defined here because it is related for this type of generator only
53
+ def filename_for(resource)
54
+ resource.id + '.html'
55
+ end
56
+
57
+ private
58
+ def copy_assets
59
+ # copy assets from template directory
60
+ source = File.join(self.templates_folder, 'assets')
61
+ target = File.join(self.documentation_dir, 'assets')
62
+ FileUtils.copy_entry(source, target)
63
+ end
64
+
65
+ def create_documentation_files
66
+ @generator = self
67
+ copy_assets
68
+ @resources = ApiSketch::Model::Resource.all
69
+ @resources.each do |resource|
70
+ @resource = resource
71
+
72
+ filename = File.join(self.documentation_dir, filename_for(@resource))
73
+ html_data = @resource_template.result(binding)
74
+
75
+ dir = File.dirname(filename)
76
+ unless File.directory?(dir)
77
+ puts_info("\t create directory: #{dir}")
78
+ FileUtils.mkdir_p(dir)
79
+ end
80
+
81
+ puts_info("\t write: #{filename}")
82
+ File.open(filename, 'w+') { |file| file.write(html_data) }
83
+ end
84
+ end
85
+ end
86
+
87
+ end
@@ -0,0 +1,21 @@
1
+ # Output info message to console.
2
+ #
3
+ # @param message [String] message to output
4
+ def puts_info(message)
5
+ $stdout.puts "\e[32m[INFO] #{message}\e[0m" if ApiSketch::Config[:debug]
6
+ end
7
+
8
+ # Output warning message to console.
9
+ #
10
+ # @param message [String] message to output
11
+ def puts_warning(message)
12
+ $stdout.puts "\e[33m[WARNING] #{message}\e[0m" if ApiSketch::Config[:debug]
13
+ end
14
+
15
+ # Output error message to console.
16
+ #
17
+ # @param message [String] message to output
18
+ def puts_error(message)
19
+ $stderr.puts "\e[31m[ERROR] #{message}\e[0m" if ApiSketch::Config[:debug]
20
+ exit(1)
21
+ end
@@ -0,0 +1,227 @@
1
+ module ApiSketch::Model
2
+
3
+ class Base
4
+
5
+ attr_accessor :name, :description
6
+
7
+ def initialize(attributes = {})
8
+ attributes = default_values_hash.merge(attributes)
9
+ attributes.each do |attribute, value|
10
+ self.send("#{attribute}=", value)
11
+ end
12
+ end
13
+
14
+ private
15
+ def default_values_hash
16
+ {}
17
+ end
18
+
19
+ end
20
+
21
+
22
+ class Attribute < ApiSketch::Model::Base
23
+ attr_accessor :data_type, :value, :example, :required, :default, :content
24
+
25
+ def example_value(defaults_allowed=false)
26
+ value = self.example
27
+ value ||= example_value_default if defaults_allowed
28
+
29
+ value.respond_to?(:call) ? value.call : value
30
+ end
31
+
32
+ # TODO: These default values should be configurable via DSL
33
+ # Some logic to defer value example from key name, - email from key with email part inside, etc.
34
+ def example_value_default
35
+ {
36
+ integer: lambda { rand(1000) + 1 },
37
+ string: lambda { "random_string_#{('A'..'Z').to_a.shuffle.first(8).join}" },
38
+ float: lambda { rand(100) + rand(100) * 0.01 },
39
+ boolean: lambda { [true, false].sample },
40
+ datetime: lambda { Time.now.strftime("%d-%m-%Y %H:%M:%S") },
41
+ timestamp: lambda { Time.now.to_i }
42
+ }[data_type]
43
+ end
44
+
45
+ def to_hash
46
+ {
47
+ data_type: self.data_type,
48
+ example_value: self.example_value,
49
+ required: !!self.required,
50
+ default: self.default,
51
+ content: self.content_to_hash
52
+ }
53
+ end
54
+
55
+ def content_to_hash
56
+ if self.content
57
+ self.content.map do |item|
58
+ item.to_hash
59
+ end
60
+ end
61
+ end
62
+
63
+ end
64
+
65
+
66
+ class Header < ApiSketch::Model::Base
67
+ attr_accessor :value, :example, :required
68
+
69
+ def example_value
70
+ self.example.respond_to?(:call) ? self.example.call : self.example
71
+ end
72
+ end
73
+
74
+
75
+ class Parameters < ApiSketch::Model::Base
76
+ attr_accessor :query, :body, :query_container_type, :body_container_type
77
+
78
+ def initialize(attributes = {})
79
+ super(attributes)
80
+ self.query ||= []
81
+ self.body ||= []
82
+ end
83
+
84
+ def as_full_names
85
+ fullname_params = self.class.new
86
+ [:query, :body].each do |param_location|
87
+ new_params = []
88
+ self.send(param_location).each do |param|
89
+ if param.data_type == :document
90
+ full_names_for(param, param.name, new_params)
91
+ else
92
+ new_params << param
93
+ end
94
+ end
95
+ fullname_params.send("#{param_location}=", new_params)
96
+ end
97
+ fullname_params
98
+ end
99
+
100
+ private
101
+ def full_names_for(param, name = "", new_params)
102
+ name = name.to_s # ensure that this value is always a string
103
+ if param.content.kind_of?(Array)
104
+ param.content.each do |attribute|
105
+ renamed_attribute = attribute.clone
106
+ renamed_attribute.name = name.empty? ? attribute.name.to_s : "#{name}[#{attribute.name}]"
107
+ if renamed_attribute.data_type == :document
108
+ full_names_for(renamed_attribute, renamed_attribute.name, new_params)
109
+ else
110
+ new_params << renamed_attribute
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
116
+
117
+
118
+ class Resource < ApiSketch::Model::Base
119
+
120
+ attr_accessor :namespace, :action, :path, :http_method, :format, :headers, :parameters, :responses
121
+
122
+ # TODO: update this method to provide better id that is used as part of filename
123
+ def id
124
+ [self.namespace, self.action].reject { |v| v.nil? || v == "" }.join("/")
125
+ end
126
+
127
+ class << self
128
+
129
+ def create(attributes)
130
+ res = self.new(attributes)
131
+ res.send(:run_validations!)
132
+ self.add(res)
133
+ res
134
+ end
135
+
136
+ def add(resource)
137
+ @resources ||= []
138
+ @resources << resource
139
+ end
140
+
141
+ def reset!
142
+ @resources = []
143
+ end
144
+
145
+ def reload!(definitions_dir)
146
+ self.reset!
147
+ ApiSketch::DSL.new(definitions_dir).init!
148
+ end
149
+
150
+ def all
151
+ @resources ||= []
152
+ end
153
+
154
+ def find(id)
155
+ self.all.find { |res| res.id == id }
156
+ end
157
+
158
+ def first
159
+ self.all.first
160
+ end
161
+
162
+ def last
163
+ self.all.last
164
+ end
165
+
166
+ def count
167
+ self.all.count
168
+ end
169
+
170
+ end
171
+
172
+ private
173
+ def default_values_hash
174
+ {
175
+ http_method: "GET",
176
+ format: "json",
177
+ headers: [],
178
+ parameters: ::ApiSketch::Model::Parameters.new,
179
+ responses: []
180
+ }
181
+ end
182
+
183
+ def run_validations!
184
+ unless self.action =~ /\A\w*\z/
185
+ message = "'#{self.action}' is invalid action value"
186
+ # puts_error(message)
187
+ raise ::ApiSketch::Error, message
188
+ end
189
+
190
+ if self.class.find(self.id)
191
+ message = "'#{self.id}' is not unique id. Change values of 'namespace' and/or 'action' attributes"
192
+ # puts_error(message)
193
+ raise ::ApiSketch::Error, message
194
+ end
195
+ end
196
+
197
+ end
198
+
199
+
200
+ class Response < ApiSketch::Model::Base
201
+ attr_accessor :http_status, :parameters, :format, :headers
202
+
203
+ private
204
+ def default_values_hash
205
+ {
206
+ format: "json",
207
+ headers: [],
208
+ parameters: ::ApiSketch::Model::Parameters.new
209
+ }
210
+ end
211
+ end
212
+
213
+ module SharedBlock
214
+ @list_hash = {}
215
+
216
+ class << self
217
+ def add(name, block)
218
+ @list_hash[name] = block
219
+ end
220
+
221
+ def find(name)
222
+ @list_hash[name] || raise(::ApiSketch::Error, "Shared block '#{name}' is not defined")
223
+ end
224
+ end
225
+ end
226
+
227
+ end
@@ -0,0 +1,48 @@
1
+ class ApiSketch::ResponseRenderer
2
+
3
+ attr_reader :params, :container_type
4
+
5
+ def initialize(params, container_type)
6
+ @params = params
7
+ @container_type = container_type
8
+ end
9
+
10
+ def to_h
11
+ render_content(params, container_type)
12
+ end
13
+
14
+ def to_json
15
+ self.to_h.to_json
16
+ end
17
+
18
+ # TODO: Add this feature in future
19
+ # def to_xml
20
+ # XML conversion code here
21
+ # end
22
+
23
+ private
24
+ def render_content(items, placeholder_type)
25
+ placeholder = case placeholder_type
26
+ when :array then []
27
+ when :document then {}
28
+ end
29
+
30
+ items.each do |param, index|
31
+ value = if [:array, :document].include?(param.data_type) && param.content
32
+ render_content(param.content, param.data_type)
33
+ else
34
+ param.example_value(true)
35
+ end
36
+
37
+ case placeholder_type
38
+ when :array
39
+ placeholder << value
40
+ when :document
41
+ placeholder[param.name] = value
42
+ end
43
+ end
44
+
45
+ placeholder
46
+ end
47
+
48
+ end