tomyum 0.1.0.a

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 +7 -0
  2. data/README.md +1106 -0
  3. data/lib/tomyum/assertions.rb +80 -0
  4. data/lib/tomyum/attributes/array.rb +11 -0
  5. data/lib/tomyum/attributes/attribute.rb +130 -0
  6. data/lib/tomyum/attributes/boolean.rb +22 -0
  7. data/lib/tomyum/attributes/currency.rb +19 -0
  8. data/lib/tomyum/attributes/date.rb +11 -0
  9. data/lib/tomyum/attributes/float.rb +13 -0
  10. data/lib/tomyum/attributes/integer.rb +14 -0
  11. data/lib/tomyum/attributes/ip_address.rb +15 -0
  12. data/lib/tomyum/attributes/number.rb +24 -0
  13. data/lib/tomyum/attributes/object.rb +71 -0
  14. data/lib/tomyum/attributes/schema.rb +23 -0
  15. data/lib/tomyum/attributes/string.rb +36 -0
  16. data/lib/tomyum/attributes/time.rb +19 -0
  17. data/lib/tomyum/attributes/uri.rb +19 -0
  18. data/lib/tomyum/attributes/visitor.rb +136 -0
  19. data/lib/tomyum/attributes.rb +92 -0
  20. data/lib/tomyum/endpoint.rb +102 -0
  21. data/lib/tomyum/endpoints/method.rb +90 -0
  22. data/lib/tomyum/endpoints/params.rb +115 -0
  23. data/lib/tomyum/error.rb +17 -0
  24. data/lib/tomyum/functions.rb +49 -0
  25. data/lib/tomyum/generators/generator.rb +16 -0
  26. data/lib/tomyum/generators/grpc/generator.rb +10 -0
  27. data/lib/tomyum/generators/open_api/generator.rb +205 -0
  28. data/lib/tomyum/generators/open_api/property_generator.rb +111 -0
  29. data/lib/tomyum/generators.rb +3 -0
  30. data/lib/tomyum/registry.rb +75 -0
  31. data/lib/tomyum/resolvable.rb +11 -0
  32. data/lib/tomyum/resolver.rb +99 -0
  33. data/lib/tomyum/serializer.rb +125 -0
  34. data/lib/tomyum/serializers/serializable.rb +23 -0
  35. data/lib/tomyum/server/app.rb +33 -0
  36. data/lib/tomyum/server/document.rb +20 -0
  37. data/lib/tomyum/server/documents/redoc.rb +36 -0
  38. data/lib/tomyum/server/documents/swagger.rb +47 -0
  39. data/lib/tomyum/server/routes.rb +0 -0
  40. data/lib/tomyum/support.rb +13 -0
  41. data/lib/tomyum/validator.rb +205 -0
  42. data/lib/tomyum/validators/normalizable.rb +24 -0
  43. data/lib/tomyum/validators/proxy.rb +77 -0
  44. data/lib/tomyum/validators/validatable.rb +48 -0
  45. data/lib/tomyum/version.rb +3 -0
  46. data/lib/tomyum.rb +28 -0
  47. metadata +202 -0
@@ -0,0 +1,92 @@
1
+ require_relative "registry"
2
+ require_relative "attributes/visitor"
3
+ require_relative "serializers/serializable"
4
+ require_relative "validators/normalizable"
5
+ require_relative "validators/validatable"
6
+
7
+ # Attributes
8
+ require_relative "attributes/boolean"
9
+ require_relative "attributes/string"
10
+ require_relative "attributes/number"
11
+ require_relative "attributes/integer"
12
+ require_relative "attributes/float"
13
+ require_relative "attributes/array"
14
+ require_relative "attributes/object"
15
+ require_relative "attributes/schema"
16
+ require_relative "attributes/date"
17
+ require_relative "attributes/time"
18
+ require_relative "attributes/currency"
19
+ require_relative "attributes/uri"
20
+ require_relative "attributes/ip_address"
21
+
22
+ module Tomyum
23
+ # Attributes registry holds the references to all attributes
24
+ #
25
+ # @see .create
26
+ module Attributes
27
+ include Tomyum::Assertions
28
+ KeyError = Tomyum::Error.create "Attribute %s already exists"
29
+
30
+ @registry = Registry.new({}, error: KeyError) do |id, name, options, block|
31
+ attribute = get(id)
32
+ attribute.new(name, options, &block)
33
+ end
34
+
35
+ class << self
36
+ # Holds reference to all attribute objects
37
+ #
38
+ # @return [Registry]
39
+ attr_accessor :registry
40
+
41
+ # Creates new attribute (Delegates to Registry#create).
42
+ #
43
+ # @return [Attribute]
44
+ #
45
+ # @example Creates primitive attribute
46
+ # Attributes.create(:string, :username, required: true)
47
+ #
48
+ # @example Creates compound attribute
49
+ # Attributes.create(:object, :user) do
50
+ # string :username
51
+ # end
52
+ def create(type, name, options = {}, &block)
53
+ registry.create(type, name, options, &block)
54
+ end
55
+
56
+ # Registers new Attribute (alias for Registry#register)
57
+ #
58
+ # @example
59
+ # register(:custom, CustomAttribute)
60
+ def register(type, klass, override: false)
61
+ registry.register(type, klass, override: override)
62
+ end
63
+
64
+ def key?(key)
65
+ registry.key?(key)
66
+ end
67
+ end
68
+
69
+ ALL = {
70
+ string: String,
71
+ number: Number,
72
+ integer: Integer,
73
+ float: Float,
74
+ boolean: Boolean,
75
+ object: Object,
76
+ array: Array,
77
+ schema: Schema,
78
+ time: Time,
79
+ date: Date,
80
+ uri: URI,
81
+ currency: Currency,
82
+ ip_address: IPAddress,
83
+ }.freeze
84
+
85
+ SCALAR_TYPES = %i[string number integer float boolean].freeze
86
+ NATIVE_TYPES = SCALAR_TYPES + %i[array object].freeze
87
+ NON_SCALAR_TYPES = ALL.keys - SCALAR_TYPES
88
+
89
+ # register pre-defined attributes
90
+ ALL.each { |name, attr| register(name, attr) }
91
+ end
92
+ end
@@ -0,0 +1,102 @@
1
+ require_relative "endpoints/params"
2
+ require_relative "endpoints/method"
3
+
4
+ module Tomyum
5
+ class Endpoint
6
+ include Tomyum::Assertions
7
+
8
+ # See `docs/api/endpoint-validations.md`.
9
+ HTTP_METHODS_WITHOUT_BODY = %i[get head trace]
10
+ HTTP_METHODS_WITH_BODY = %i[post put patch delete]
11
+ HTTP_METHODS = HTTP_METHODS_WITHOUT_BODY + HTTP_METHODS_WITH_BODY
12
+
13
+ class << self
14
+ # Converts `/users/:id/friends` to `users_id_friends`
15
+ def normalize_name(name)
16
+ name.to_s.delete(":").gsub(/(\A\/+|\/+\z)/, '').tr('/', '_').to_sym
17
+ end
18
+ end
19
+
20
+ DEFAULT_OPTIONS = {
21
+ schema: nil,
22
+ title: nil,
23
+ description: nil,
24
+ }.freeze
25
+
26
+ attr_reader :path, :name, :schema
27
+ attr_accessor :title, :description
28
+
29
+ def initialize(path, options = {}, &block)
30
+ options = DEFAULT_OPTIONS.merge(options)
31
+ @schema = options[:schema]
32
+ @path = path
33
+ @name = normalize_name(options[:name])
34
+ @methods = {}
35
+ @params = nil
36
+ @block = block
37
+ @loaded = false
38
+ @title = options[:title]
39
+ @description = options[:description]
40
+ end
41
+
42
+ # Defines http methods dynamically e.g. :get, :post ...
43
+ # endpoint.define do
44
+ # get :index, path: "/"
45
+ # post :create
46
+ # end
47
+ HTTP_METHODS.each do |method|
48
+ define_method method do |action, path: "/", **options|
49
+ create_method(method, action, path: path, **options)
50
+ end
51
+ end
52
+
53
+ # Lazily builds all params and return methods hash
54
+ def methods(resolver = nil)
55
+ ensure_loaded
56
+
57
+ @methods.each { |name, _| method(name, resolver: resolver) }
58
+ end
59
+
60
+ # Lazily builds params for speicified method
61
+ def method(name, resolver: nil)
62
+ ensure_loaded
63
+ assert_in @methods, name.to_sym
64
+
65
+ @methods[name.to_sym].tap { |method| method.build_params(params, resolver) }
66
+ end
67
+
68
+ # Returns default params
69
+ def params(&block)
70
+ ensure_loaded
71
+
72
+ @params ||= Tomyum::Endpoints::Params.new(:default, schema: schema, built: true, &block || -> {})
73
+ end
74
+
75
+ # Registers http verb method
76
+ #
77
+ # create_method(:post, :create, path: "/")
78
+ def create_method(verb, action, path: "/", **options)
79
+ # Automatically adds it self as schema value
80
+ options[:schema] = options.fetch(:schema, schema)
81
+
82
+ @methods[action.to_sym] = Tomyum::Endpoints::Method.new(verb, action: action, path: path, **options)
83
+ end
84
+
85
+ private
86
+
87
+ def normalize_name(name)
88
+ (name || @schema || self.class.normalize_name(@path)).to_sym
89
+ end
90
+
91
+ # Lazily executes &block
92
+ def ensure_loaded
93
+ return if @loaded
94
+
95
+ # We need to set status here; otherwise, we'll have
96
+ # error when there's nested block.
97
+ @loaded = true
98
+
99
+ instance_exec(&@block) if @block
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,90 @@
1
+ module Tomyum
2
+ module Endpoints
3
+ # Represents HTTP method
4
+ #
5
+ # @example Create new +POST+ {Method}
6
+ # Method.new(:post, action: :create, path: "/", schema: :user, as: :list)
7
+ #
8
+ # @example
9
+ # Method.new(:post, {
10
+ # action: :create,
11
+ # path: "/",
12
+ # validators: [CustomValidator]
13
+ # })
14
+ class Method
15
+ DEFAULT_OPTIONS = {
16
+ as: nil,
17
+ params: [],
18
+ exclude: false,
19
+ schema: nil,
20
+ status: 200,
21
+ title: nil,
22
+ description: nil,
23
+ validators: [],
24
+ }.freeze
25
+
26
+ attr_reader :verb, :path, :action, :params, :schema, :status, :as
27
+ attr_accessor :title, :description
28
+
29
+ def initialize(verb, action:, path: "/", **options)
30
+ @options = DEFAULT_OPTIONS.merge(options)
31
+ @verb = verb
32
+ @path = path
33
+ @action = action
34
+
35
+ # Create ParamsAttribute instance
36
+ @params = create_params(action, **options.slice(:params, :schema, :exclude))
37
+ @schema = @options[:schema]
38
+ @as = @options[:as]
39
+ @status = @options[:status]
40
+ @title = @options[:title]
41
+ @description = @options[:description]
42
+ @validator = nil
43
+ @validators = @options[:validators]
44
+ @resolver = nil
45
+ @result = nil
46
+ end
47
+
48
+ def validate(params = {}, resolver = nil)
49
+ @validator = create_validator(@resolver || resolver)
50
+ @result = @validator.validate(@params, params, validators: @validators)
51
+ end
52
+
53
+ def valid?(*args)
54
+ validate(*args)
55
+
56
+ @result&.errors&.empty?
57
+ end
58
+
59
+ def errors
60
+ @result&.errors
61
+ end
62
+
63
+ # Does it have request body? (only non GET verb can have request body)
64
+ def body?
65
+ Tomyum::Endpoint::HTTP_METHODS_WITH_BODY.include?(@verb) && @params.keys.any?
66
+ end
67
+
68
+ def build_params(default = {}, resolver = nil)
69
+ @resolver = resolver if resolver
70
+ @params = @params.build(default, resolver)
71
+ end
72
+
73
+ def permit_params(params = {}, target = nil)
74
+ return params unless params.respond_to?(:permit)
75
+
76
+ params.permit(*@params.permit(params))
77
+ end
78
+
79
+ private
80
+
81
+ def create_params(action, **options)
82
+ Tomyum::Endpoints::Params.new(action, **options)
83
+ end
84
+
85
+ def create_validator(*args)
86
+ Tomyum::Validator.new(*args)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,115 @@
1
+ module Tomyum
2
+ module Endpoints
3
+ # Usage
4
+ #
5
+ # Params.new(:create)
6
+ # Params.new(:index, params: :list)
7
+ # Params.new(:create, params: %i[amount currency])
8
+ # Params.new(:create, params: %i[amount], exclude: true)
9
+ # Params.new(:create, params: false)
10
+ # Params.new :create, params: -> {
11
+ # integer :amount
12
+ # }
13
+ # Params.new(:index, schema: :user, as: :list)
14
+ #
15
+ class Params < Attributes::Object
16
+ include Tomyum::Assertions
17
+ include Tomyum::Resolvable
18
+
19
+ InvalidType = Tomyum::Error.create("Invalid params type")
20
+ NotBuilt = Tomyum::Error.create("Params need to be built first")
21
+
22
+ DEFAULT_OPTIONS = {
23
+ exclude: false,
24
+ schema: nil,
25
+ built: false,
26
+ }.freeze
27
+
28
+ attr_reader :action, :params, :schema
29
+
30
+ def initialize(action, params: false, **options, &block)
31
+ super(action, options.except(DEFAULT_OPTIONS.keys))
32
+
33
+ options = DEFAULT_OPTIONS.merge(options)
34
+ @action = action.to_sym
35
+ @exclude = options[:exclude]
36
+ @schema = options[:schema]
37
+ @params = block_given? ? block : params
38
+ @built = options[:built]
39
+ end
40
+
41
+ def build(default = {}, resolver = nil)
42
+ # Flag as built
43
+ @built = true
44
+
45
+ case @params
46
+ when Params
47
+ @params
48
+ when true
49
+ # use default params
50
+ default
51
+ when false
52
+ # use no params
53
+ self
54
+ when Proc
55
+ instance_exec(&@params)
56
+ self
57
+ when Symbol
58
+ # use params from other service
59
+ resolver.endpoint(@params).params
60
+ when Array
61
+ assert_subset_of(default.keys, @params)
62
+
63
+ # override default params by using specified symbol
64
+ keys = @exclude ? default.keys - @params : @params
65
+ keys.each { |name| self[name] = default[name] }
66
+ self
67
+ else
68
+ raise InvalidType
69
+ end
70
+ end
71
+
72
+ def built?
73
+ @built
74
+ end
75
+
76
+ # Build permitted params for ActionController::Params
77
+ def permit(params = {})
78
+ raise NotBuilt unless built?
79
+
80
+ build_permitted_params(attributes, params)
81
+ end
82
+
83
+ private
84
+
85
+ def build_permitted_params(attrs, params = {})
86
+ attrs.map do |name, attr|
87
+ # permit array
88
+ # TODO: params.permit(:foo, array: [:key1, :key2])
89
+ next { name => [] } if attr.kind_of?(Tomyum::Attributes::Array)
90
+
91
+ # permit non-object
92
+ next name unless attr.kind_of?(Tomyum::Attributes::Object)
93
+
94
+ # Rails 5.0 doesn't support arbitary hash params
95
+ # 5.1+ supports this, via `key: {}` empty hash.
96
+ #
97
+ # The work around is to get hash keys from params
98
+ # and assign them to permit keys
99
+ # params = params&.fetch(name, {})
100
+ data = params&.fetch(name, {})
101
+ keys = build_permitted_params(attr.attributes, data)
102
+
103
+ { attr.name => normalize_permitted_params(keys, data) }
104
+ end
105
+ end
106
+
107
+ def normalize_permitted_params(keys, params = {})
108
+ return keys unless keys.empty?
109
+ return {} if params.empty?
110
+
111
+ params.keys
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,17 @@
1
+ module Tomyum
2
+ # @example
3
+ # InvaldKey = Error.create("Invalid %s key")
4
+ # raise InvalidKey, "id"
5
+ class Error < StandardError
6
+ def self.create(msg)
7
+ Class.new(Error) do
8
+ MESSAGE.replace msg
9
+ end
10
+ end
11
+
12
+ MESSAGE = "Error"
13
+ def initialize(*args)
14
+ super args.any? ? MESSAGE % args : MESSAGE
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,49 @@
1
+ module Tomyum
2
+ # FP helpers
3
+ module Functions
4
+ class Result
5
+ attr_reader :value, :errors
6
+
7
+ def initialize(value, errors = nil)
8
+ @value = value
9
+ @errors = errors
10
+ end
11
+
12
+ def compose(f)
13
+ return self if errors
14
+
15
+ f.(value)
16
+ end
17
+ end
18
+
19
+ def result(value, errors = nil)
20
+ Result.new(value, errors)
21
+ end
22
+
23
+ # Compose functions
24
+ # compose : (a -> b) -> (b -> c) -> a -> c
25
+ def compose(*fns)
26
+ ->(v) {
27
+ fns.reduce(fns.shift.(v)) do |x, f|
28
+ x.respond_to?(:compose) ? x.compose(f) : f.(x)
29
+ end
30
+ }
31
+ end
32
+
33
+ # A composition that early returns value
34
+ # f : a -> b
35
+ # g : a -> c
36
+ def compose_result(*fns)
37
+ ->(v) {
38
+ fns.reduce(fns.shift.(v)) do |x, f|
39
+ x ? x : f.(v)
40
+ end
41
+ }
42
+ end
43
+
44
+ # Curry instance method
45
+ def curry(method, *args)
46
+ self.method(method).curry[*args]
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,16 @@
1
+ module Tomyum
2
+ module Generators
3
+ class Generator
4
+ include Tomyum::Assertions
5
+ include Tomyum::Resolvable
6
+
7
+ def initialize(resolver = nil)
8
+ @resolver = resolver
9
+ end
10
+
11
+ def generate(*args)
12
+ raise NotImplementedError
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,10 @@
1
+ module Tomyum
2
+ module API
3
+ module Generators
4
+ module Grpc
5
+ class Generator
6
+ end
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,205 @@
1
+ require "ostruct"
2
+
3
+ module Tomyum
4
+ module Generators
5
+ module OpenAPI
6
+ # Generate Open API v3 Specifications
7
+ #
8
+ # @example
9
+ # resolver = Resolver.new(schemas: [])
10
+ # generator = OpenApi::Generator.new(resolver, {
11
+ # title: "Awesome API",
12
+ # version: "1.0.0",
13
+ # # other infos ...
14
+ # })
15
+ #
16
+ # generator = OpenApi::Generator.new(resolver) do
17
+ # title "Awesome API"
18
+ # version "1.0.0"
19
+ # servers []
20
+ # end
21
+ #
22
+ # generator.generate # => { }
23
+ class Generator < Tomyum::Generators::Generator
24
+ OPEN_API_VERSION = "3.0.0".freeze
25
+ OPEN_API_ID_REGEX = /:([\w]+)/.freeze
26
+ EMPTY_RESPONSE = "Response".freeze
27
+
28
+ OPTIONS = {
29
+ version: nil,
30
+ title: nil,
31
+ description: nil,
32
+ servers: []
33
+ }
34
+
35
+ def initialize(resolver = nil, options = {})
36
+ super(resolver)
37
+
38
+ @options = OPTIONS.merge(options)
39
+ end
40
+
41
+ def generate
42
+ {
43
+ openapi: OPEN_API_VERSION,
44
+ info: {
45
+ version: @options[:version],
46
+ title: @options[:title],
47
+ description: @options[:description],
48
+ },
49
+ servers: generate_server_urls(@options[:servers]),
50
+ paths: generate_paths(resolver.endpoints.keys),
51
+ components: {
52
+ schemas: generate_schemas(resolver.schemas.keys),
53
+ },
54
+ }
55
+ end
56
+
57
+ # share for both specs
58
+ def generate_params(method, base_url = nil)
59
+ generate_path_params(method, base_url) + generate_query_params(method)
60
+ end
61
+
62
+ def generate_param(param, in: :query, schema: nil)
63
+ return unless param.serializable?
64
+
65
+ {
66
+ name: param.name.to_s,
67
+ in: binding.local_variable_get(:in),
68
+ schema: PropertyGenerator.generate(param, schema: schema),
69
+ }.tap do |parameter|
70
+ parameter[:required] = param.required if param.required
71
+ parameter[:deprecated] = param.deprecated if param.deprecated
72
+ parameter[:description] = param.description if param.description
73
+ end
74
+ end
75
+
76
+ # path contains id? e.g. /users/:id
77
+ def generate_path_params(method, base_url = nil)
78
+ extract_ids(url(base_url, method.path))
79
+ .map { |id| Attributes::String.new(id, required: true) }
80
+ .map { |id| generate_param(id, in: :path) }
81
+ end
82
+
83
+ # convert request body to `query` params when HTTP verb is `GET`
84
+ def generate_query_params(method)
85
+ return [] unless method.verb == :get
86
+
87
+ method.params.attributes.values.each_with_object([]) do |param, params|
88
+ # include original schema when method.schema differs from params.schema
89
+ schema = method.params.schema if method.schema != method.params.schema
90
+ params << generate_param(param, schema: schema)
91
+ end.compact
92
+ end
93
+
94
+ def group_methods_by_path(methods)
95
+ methods.values.each_with_object({}) do |method, paths|
96
+ paths[method.path] ||= []
97
+ paths[method.path] << method
98
+ end
99
+ end
100
+
101
+ # -- end --
102
+ def generate_server_urls(urls = [])
103
+ urls.map { |name, url| { url: url, description: name } }
104
+ end
105
+
106
+ def generate_paths(services)
107
+ services.each_with_object({}) do |name, paths|
108
+ # no need to initialize
109
+ next unless (endpoint = resolver.endpoint(name))
110
+
111
+ generate_operations(endpoint, paths)
112
+ end
113
+ end
114
+
115
+ def generate_operations(endpoint, paths)
116
+ base_url = endpoint.path
117
+ group_methods_by_path(endpoint.methods(resolver)).each do |path, methods|
118
+ resource_url = convert_id_params(url(base_url, path))
119
+ paths[resource_url] = {}
120
+
121
+ methods.each do |method|
122
+ paths[resource_url][method.verb] = generate_operation(method, base_url)
123
+ end
124
+ end
125
+ end
126
+
127
+ # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#operationObject
128
+ def generate_operation(method, base_url = nil)
129
+ {
130
+ tags: [method.schema.to_s.humanize.pluralize],
131
+ parameters: generate_params(method, base_url),
132
+ }.tap do |operation|
133
+ operation[:requestBody] = generate_request_params(method) if method.body?
134
+ operation[:summary] = method.description if method.description
135
+ operation[:responses] = generate_responses(method)
136
+ end
137
+ end
138
+
139
+ def generate_request_params(method)
140
+ {
141
+ "content": {
142
+ "application/x-www-form-urlencoded": {
143
+ schema: generate_schema(method.params),
144
+ },
145
+ },
146
+ }
147
+ end
148
+
149
+ def generate_responses(method)
150
+ {
151
+ method.status => generate_response(method),
152
+ }
153
+ end
154
+
155
+ def generate_response(method)
156
+ {
157
+ description: method.schema&.to_s&.humanize || EMPTY_RESPONSE,
158
+ }.tap do |response|
159
+ response[:content] = generate_json_response(method) if method.schema.present?
160
+ end
161
+ end
162
+
163
+ def generate_json_response(method)
164
+ {
165
+ "application/json": {
166
+ schema: PropertyGenerator.generate(method.schema, as: method.as),
167
+ },
168
+ }
169
+ end
170
+
171
+ # https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#schemaObject
172
+ def generate_schemas(serializers)
173
+ serializers.each_with_object({}) do |name, schemas|
174
+ # no need to initialize
175
+ next unless (object = resolver.schema(name))
176
+
177
+ schemas[name] = generate_schema(object)
178
+ end
179
+ end
180
+
181
+ def generate_schema(object)
182
+ self.class.assert_kind_of Tomyum::Attributes::Attribute, object
183
+
184
+ PropertyGenerator.generate(object)
185
+ end
186
+
187
+ private
188
+
189
+ # convert :id to {id} format
190
+ def convert_id_params(url)
191
+ url.gsub(OPEN_API_ID_REGEX, '{\1}')
192
+ end
193
+
194
+ def extract_ids(path)
195
+ path.scan(OPEN_API_ID_REGEX).flatten
196
+ end
197
+
198
+ # join relative url
199
+ def url(*parts)
200
+ "/" + parts.compact.map { |part| part.gsub(/^\/|\/$/, "") }.reject(&:blank?).join("/")
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end