api_sketch 0.1.1 → 0.1.2

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/CHANGELOG.md +7 -2
  4. data/README.md +63 -9
  5. data/api_sketch.gemspec +0 -1
  6. data/examples/api_project/config/initializers/shared_blocks.rb +15 -0
  7. data/examples/api_project/resources/places.rb +138 -0
  8. data/examples/api_project/resources/users/points/stats.rb +30 -0
  9. data/examples/api_project/resources/users/points.rb +32 -0
  10. data/examples/api_project/resources/users.rb +341 -0
  11. data/lib/api_sketch/config.rb +1 -1
  12. data/lib/api_sketch/dsl/attribute_parser.rb +19 -0
  13. data/lib/api_sketch/dsl/attributes.rb +45 -0
  14. data/lib/api_sketch/dsl/base.rb +8 -0
  15. data/lib/api_sketch/dsl/complex_attribute_parser.rb +11 -0
  16. data/lib/api_sketch/dsl/headers.rb +17 -0
  17. data/lib/api_sketch/dsl/parameters.rb +31 -0
  18. data/lib/api_sketch/dsl/responses.rb +22 -0
  19. data/lib/api_sketch/dsl.rb +27 -165
  20. data/lib/api_sketch/examples_server.rb +1 -1
  21. data/lib/api_sketch/generators/base.rb +42 -0
  22. data/lib/api_sketch/{generators.rb → generators/bootstrap.rb} +2 -46
  23. data/lib/api_sketch/model/attribute.rb +42 -0
  24. data/lib/api_sketch/model/base.rb +17 -0
  25. data/lib/api_sketch/model/header.rb +7 -0
  26. data/lib/api_sketch/model/parameters.rb +50 -0
  27. data/lib/api_sketch/model/resource.rb +99 -0
  28. data/lib/api_sketch/model/response.rb +12 -0
  29. data/lib/api_sketch/model/shared_block.rb +17 -0
  30. data/lib/api_sketch/model.rb +0 -242
  31. data/lib/api_sketch/{renderers.rb → response_renderer.rb} +0 -0
  32. data/lib/api_sketch/version.rb +1 -1
  33. data/lib/api_sketch.rb +20 -3
  34. data/spec/lib/api_sketch/dsl/attribute_parser_spec.rb +14 -0
  35. data/spec/lib/api_sketch/dsl/attributes_spec.rb +14 -0
  36. data/spec/lib/api_sketch/dsl/complex_attribute_parser_spec.rb +14 -0
  37. data/spec/lib/api_sketch/dsl/headers_spec.rb +14 -0
  38. data/spec/lib/api_sketch/dsl/parameters_spec.rb +14 -0
  39. data/spec/lib/api_sketch/dsl/responses_spec.rb +14 -0
  40. data/spec/lib/api_sketch/dsl_spec.rb +5 -3
  41. data/spec/lib/api_sketch/model/parameters_spec.rb +28 -0
  42. data/spec/lib/api_sketch/{renderers_spec.rb → response_renderer_spec.rb} +51 -1
  43. data/spec/spec_helper.rb +3 -1
  44. data/spec/support/shared_examples.rb +9 -0
  45. metadata +43 -9
  46. data/.ruby-gemset +0 -1
  47. data/.ruby-version +0 -1
@@ -1,5 +1,7 @@
1
1
  class ApiSketch::DSL
2
2
 
3
+ attr_reader :definitions_dir
4
+
3
5
  COMPLEX_ATTRIBUTE_NAMES = [:headers, :parameters, :responses]
4
6
 
5
7
  def initialize(definitions_dir=ApiSketch::Config[:definitions_dir])
@@ -7,170 +9,18 @@ class ApiSketch::DSL
7
9
  end
8
10
 
9
11
  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
12
+ if File.directory?(config_dir)
13
+ puts_info("Load configuration")
14
+ load_dir_files(config_dir)
50
15
  end
51
16
 
52
- def shared(name)
53
- self.instance_eval(&::ApiSketch::Model::SharedBlock.find(name))
17
+ if File.directory?(resources_dir)
18
+ puts_info("Load resources")
19
+ load_dir_files(resources_dir)
54
20
  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
21
  end
98
22
 
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)
23
+ def shared_block(name, &block)
174
24
  ::ApiSketch::Model::SharedBlock.add(name, block)
175
25
  end
176
26
 
@@ -183,16 +33,12 @@ class ApiSketch::DSL
183
33
  end
184
34
 
185
35
  # Assign resource namespace
186
- attributes[:namespace] ||= block.source_location[0].gsub(definitions_dir, "").gsub(".rb", "").split("/").reject { |ns| ns.nil? || ns == "" }.join("/")
36
+ attributes[:namespace] ||= block.source_location[0].gsub(resources_dir, "").gsub(".rb", "").split("/").reject { |ns| ns.nil? || ns == "" }.join("/")
187
37
 
188
38
  ::ApiSketch::Model::Resource.create(attributes)
189
39
  end
190
40
 
191
-
192
41
  private
193
- def definitions_dir
194
- @definitions_dir
195
- end
196
42
 
197
43
  def get_attrs(name, &block)
198
44
  ::ApiSketch::DSL::AttributeParser.new(:root, &block).to_h.merge(name: name)
@@ -210,4 +56,20 @@ class ApiSketch::DSL
210
56
  end
211
57
  end
212
58
 
213
- end
59
+ # Definitions loading
60
+ def config_dir
61
+ "#{definitions_dir}/config"
62
+ end
63
+
64
+ def resources_dir
65
+ "#{definitions_dir}/resources"
66
+ end
67
+
68
+ def load_dir_files(dir)
69
+ Dir.glob("#{dir}/**/*.rb").each do |file_path|
70
+ puts_info("\t read: #{file_path}")
71
+ binding.eval(File.open(File.expand_path(file_path)).read, file_path)
72
+ end
73
+ end
74
+
75
+ end
@@ -25,7 +25,7 @@ class ApiSketch::ExamplesServer
25
25
 
26
26
  response.status = Rack::Utils.status_code(api_response.http_status)
27
27
 
28
- response.write(ApiSketch::ResponseRenderer.new(api_response.parameters.body, api_response.parameters.body_container_type, get_elements_count).to_json)
28
+ response.write(ApiSketch::ResponseRenderer.new([api_response.parameters.wrapped_body], api_response.parameters.body_container_type, get_elements_count).to_json)
29
29
  end
30
30
  else
31
31
  api_sketch_message("No any responses defined for this resource and context", 404)
@@ -0,0 +1,42 @@
1
+ module ApiSketch::Generators
2
+ class Base
3
+
4
+ attr_accessor :definitions_dir, :documentation_dir
5
+
6
+ attr_reader :templates_folder
7
+
8
+ # TODO: Add here some validations for folders existance, etc
9
+ def initialize(options = {})
10
+ self.definitions_dir = options[:definitions_dir]
11
+ self.documentation_dir = options[:documentation_dir]
12
+ @templates_folder = File.expand_path("../templates/#{self.class.name.split("::").last.downcase}", File.dirname(__FILE__))
13
+ end
14
+
15
+ def generate!
16
+ puts_info("Load definitions")
17
+ load_definitions
18
+ puts_info("Create documentation directory")
19
+ puts_info("\t path: #{self.documentation_dir}")
20
+ create_documentation_directory
21
+ puts_info("Create documentation files")
22
+ create_documentation_files
23
+ end
24
+
25
+ private
26
+ def create_documentation_directory
27
+ FileUtils.rm_r(self.documentation_dir, :force => true)
28
+ FileUtils.mkdir_p(self.documentation_dir)
29
+ end
30
+
31
+ # TODO: This is unfinished sample file generator it should be more complex at some other generators
32
+ # Other generors should inherit from this class and implement this method
33
+ def create_documentation_files
34
+ raise "This method should be implemented at child class who inherits from ApiSketch::Generators::Base"
35
+ end
36
+
37
+ def load_definitions
38
+ ApiSketch::Model::Resource.reload!(self.definitions_dir)
39
+ end
40
+
41
+ end
42
+ end
@@ -1,52 +1,9 @@
1
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
2
  class Bootstrap < ApiSketch::Generators::Base
46
3
 
47
4
  # Generated folders structure is
48
- # docs - html folders and files
49
- # assets - js, css and images. Html styling
5
+ # docs - html folders and files
6
+ # assets - js, css and images. Html styling
50
7
 
51
8
  def initialize(options = {})
52
9
  super(options)
@@ -93,5 +50,4 @@ module ApiSketch::Generators
93
50
  end
94
51
  end
95
52
  end
96
-
97
53
  end
@@ -0,0 +1,42 @@
1
+ class ApiSketch::Model::Attribute < ApiSketch::Model::Base
2
+ attr_accessor :data_type, :value, :example, :required, :default, :content
3
+
4
+ def example_value(defaults_allowed=false)
5
+ value = self.example
6
+ value ||= example_value_default if defaults_allowed
7
+
8
+ value.respond_to?(:call) ? value.call : value
9
+ end
10
+
11
+ # TODO: These default values should be configurable via DSL
12
+ # Some logic to defer value example from key name, - email from key with email part inside, etc.
13
+ def example_value_default
14
+ {
15
+ integer: lambda { rand(1000) + 1 },
16
+ string: lambda { "random_string_#{('A'..'Z').to_a.shuffle.first(8).join}" },
17
+ float: lambda { rand(100) + rand(100) * 0.01 },
18
+ boolean: lambda { [true, false].sample },
19
+ datetime: lambda { Time.now.strftime("%d-%m-%Y %H:%M:%S") },
20
+ timestamp: lambda { Time.now.to_i }
21
+ }[data_type]
22
+ end
23
+
24
+ def to_hash
25
+ {
26
+ data_type: self.data_type,
27
+ example_value: self.example_value,
28
+ required: !!self.required,
29
+ default: self.default,
30
+ content: self.content_to_hash
31
+ }
32
+ end
33
+
34
+ def content_to_hash
35
+ if self.content
36
+ self.content.map do |item|
37
+ item.to_hash
38
+ end
39
+ end
40
+ end
41
+
42
+ end
@@ -0,0 +1,17 @@
1
+ class ApiSketch::Model::Base
2
+
3
+ attr_accessor :name, :description
4
+
5
+ def initialize(attributes = {})
6
+ attributes = default_values_hash.merge(attributes)
7
+ attributes.each do |attribute, value|
8
+ self.send("#{attribute}=", value)
9
+ end
10
+ end
11
+
12
+ private
13
+ def default_values_hash
14
+ {}
15
+ end
16
+
17
+ end
@@ -0,0 +1,7 @@
1
+ class ApiSketch::Model::Header < ApiSketch::Model::Base
2
+ attr_accessor :value, :example, :required
3
+
4
+ def example_value
5
+ self.example.respond_to?(:call) ? self.example.call : self.example
6
+ end
7
+ end
@@ -0,0 +1,50 @@
1
+ class ApiSketch::Model::Parameters < ApiSketch::Model::Base
2
+ attr_accessor :query, :body, :query_container_type, :body_container_type
3
+
4
+ def initialize(attributes = {})
5
+ super(attributes)
6
+ self.query ||= []
7
+ self.body ||= []
8
+ end
9
+
10
+ def wrapped_query
11
+ ApiSketch::Model::Attribute.new(data_type: self.query_container_type, content: self.query)
12
+ end
13
+
14
+ def wrapped_body
15
+ ApiSketch::Model::Attribute.new(data_type: self.body_container_type, content: self.body)
16
+ end
17
+
18
+ def as_full_names
19
+ fullname_params = self.class.new
20
+ [:query, :body].each do |param_location|
21
+ new_params = []
22
+ self.send(param_location).each do |param|
23
+ if param.data_type == :document
24
+ full_names_for(param, param.name, new_params)
25
+ else
26
+ new_params << param
27
+ end
28
+ end
29
+ fullname_params.send("#{param_location}=", new_params)
30
+ end
31
+ fullname_params
32
+ end
33
+
34
+ private
35
+ def full_names_for(param, name = "", new_params)
36
+ name = name.to_s # ensure that this value is always a string
37
+ if param.content.kind_of?(Array)
38
+ param.content.each do |attribute|
39
+ renamed_attribute = attribute.clone
40
+ renamed_attribute.name = name.empty? ? attribute.name.to_s : "#{name}[#{attribute.name}]"
41
+ if renamed_attribute.data_type == :document
42
+ full_names_for(renamed_attribute, renamed_attribute.name, new_params)
43
+ else
44
+ new_params << renamed_attribute
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+
@@ -0,0 +1,99 @@
1
+ class ApiSketch::Model::Resource < ApiSketch::Model::Base
2
+
3
+ attr_accessor :namespace, :action, :path, :http_method, :format, :headers, :parameters, :responses
4
+
5
+ # TODO: update this method to provide better id that is used as part of filename
6
+ def id
7
+ [self.namespace, self.action].reject { |v| v.nil? || v == "" }.join("/")
8
+ end
9
+
10
+ class << self
11
+
12
+ def create(attributes)
13
+ res = self.new(attributes)
14
+ res.send(:run_validations!)
15
+ self.add(res)
16
+ res
17
+ end
18
+
19
+ def add(resource)
20
+ @resources ||= []
21
+ @resources << resource
22
+ end
23
+
24
+ def reset!
25
+ @resources = []
26
+ end
27
+
28
+ def reload!(definitions_dir)
29
+ ApiSketch::Model::SharedBlock.reset!
30
+ self.reset!
31
+ ApiSketch::DSL.new(definitions_dir).init!
32
+ end
33
+
34
+ def all
35
+ @resources ||= []
36
+ end
37
+
38
+ def find(id)
39
+ self.all.find { |res| res.id == id }
40
+ end
41
+
42
+ def find_by_http_method_and_path(http_method, path)
43
+ self.all.find { |res| res.http_method == http_method && res.path == path }
44
+ end
45
+
46
+ def first
47
+ self.all.first
48
+ end
49
+
50
+ def last
51
+ self.all.last
52
+ end
53
+
54
+ def count
55
+ self.all.count
56
+ end
57
+
58
+ end
59
+
60
+ private
61
+ def default_values_hash
62
+ {
63
+ http_method: "GET",
64
+ format: "json",
65
+ headers: [],
66
+ parameters: ::ApiSketch::Model::Parameters.new,
67
+ responses: []
68
+ }
69
+ end
70
+
71
+ def error_message(message)
72
+ # puts_error(message)
73
+ raise ::ApiSketch::Error, message
74
+ end
75
+
76
+ def run_validations!
77
+ unless self.action =~ /\A\w*\z/
78
+ error_message("'#{self.action}' is invalid action value")
79
+ end
80
+
81
+ if self.class.find(self.id)
82
+ error_message("'#{self.id}' is not unique id. Change values of 'namespace' and/or 'action' attributes")
83
+ end
84
+
85
+ if self.http_method.nil? || self.http_method.empty?
86
+ error_message("request http_method can't be blank")
87
+ end
88
+
89
+ if self.path.nil? || self.path.empty?
90
+ error_message("request path can't be blank")
91
+ end
92
+
93
+ if self.class.find_by_http_method_and_path(self.http_method, self.path)
94
+ error_message("Route '#{self.http_method} #{self.path}' should be unique")
95
+ end
96
+ end
97
+
98
+ end
99
+
@@ -0,0 +1,12 @@
1
+ class ApiSketch::Model::Response < ApiSketch::Model::Base
2
+ attr_accessor :http_status, :parameters, :format, :headers
3
+
4
+ private
5
+ def default_values_hash
6
+ {
7
+ format: "json",
8
+ headers: [],
9
+ parameters: ::ApiSketch::Model::Parameters.new
10
+ }
11
+ end
12
+ end
@@ -0,0 +1,17 @@
1
+ module ApiSketch::Model::SharedBlock
2
+ @list_hash = {}
3
+
4
+ class << self
5
+ def add(name, block)
6
+ @list_hash[name] = block
7
+ end
8
+
9
+ def reset!
10
+ @list_hash = {}
11
+ end
12
+
13
+ def find(name)
14
+ @list_hash[name] || raise(::ApiSketch::Error, "Shared block '#{name}' is not defined")
15
+ end
16
+ end
17
+ end