api_sketch 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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