tomyum 0.1.0.a

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 +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