svelte 0.1.1
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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.rubocop.yml +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +4 -0
- data/.yardopts +1 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +229 -0
- data/Rakefile +9 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/lib/svelte.rb +37 -0
- data/lib/svelte/configuration.rb +20 -0
- data/lib/svelte/errors.rb +4 -0
- data/lib/svelte/errors/http_error.rb +17 -0
- data/lib/svelte/errors/json_error.rb +4 -0
- data/lib/svelte/errors/parameter_error.rb +5 -0
- data/lib/svelte/errors/version_error.rb +9 -0
- data/lib/svelte/generic_operation.rb +63 -0
- data/lib/svelte/model_factory.rb +171 -0
- data/lib/svelte/model_factory/parameter.rb +154 -0
- data/lib/svelte/operation.rb +31 -0
- data/lib/svelte/operation_builder.rb +47 -0
- data/lib/svelte/path.rb +41 -0
- data/lib/svelte/path_builder.rb +35 -0
- data/lib/svelte/rest_client.rb +44 -0
- data/lib/svelte/service.rb +38 -0
- data/lib/svelte/string_manipulator.rb +66 -0
- data/lib/svelte/swagger_builder.rb +94 -0
- data/lib/svelte/version.rb +4 -0
- data/svelte.gemspec +33 -0
- metadata +219 -0
@@ -0,0 +1,63 @@
|
|
1
|
+
module Svelte
|
2
|
+
# Class that handles the actual execution of dynamically generated operations
|
3
|
+
# Each created operation will eventually call this class in order to make the
|
4
|
+
# final HTTP request to the REST endpoint
|
5
|
+
class GenericOperation
|
6
|
+
class << self
|
7
|
+
# Make an HTTP request to a REST resource
|
8
|
+
# @param verb [String] http verb to use, i.e. `'get'`
|
9
|
+
# @param path [Path] Path object containing information about the
|
10
|
+
# operation to be called
|
11
|
+
# @param configuration [Configuration] Swagger API configuration
|
12
|
+
# @param parameters [Hash] payload of the request, i.e. `{ petId: 1}`
|
13
|
+
# @param options [Hash] request options, i.e. `{ timeout: 10 }`
|
14
|
+
def call(verb:, path:, configuration:, parameters:, options:)
|
15
|
+
url = url_for(configuration: configuration,
|
16
|
+
path: path,
|
17
|
+
parameters: parameters)
|
18
|
+
request_parameters = clean_parameters(path: path,
|
19
|
+
parameters: parameters)
|
20
|
+
RestClient.call(verb: verb,
|
21
|
+
url: url,
|
22
|
+
params: request_parameters,
|
23
|
+
options: options)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def url_for(configuration:, path:, parameters:)
|
29
|
+
url_path =
|
30
|
+
[
|
31
|
+
path.non_parameter_elements,
|
32
|
+
named_parameters(path: path, parameters: parameters)
|
33
|
+
].flatten.join('/')
|
34
|
+
|
35
|
+
protocol = configuration.protocol
|
36
|
+
host = configuration.host
|
37
|
+
base_path = configuration.base_path
|
38
|
+
if base_path == '/'
|
39
|
+
base_path = ''
|
40
|
+
end
|
41
|
+
"#{protocol}://#{host}#{base_path}/#{url_path}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def named_parameters(path:, parameters:)
|
45
|
+
path.parameter_elements.map do |parameter_element|
|
46
|
+
unless parameters.key?(parameter_element)
|
47
|
+
raise ParameterError,
|
48
|
+
"Required parameter `#{parameter_element}` missing"
|
49
|
+
end
|
50
|
+
parameters[parameter_element]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def clean_parameters(path:, parameters:)
|
55
|
+
clean_parameters = parameters.dup
|
56
|
+
path.parameter_elements.each do |parameter_element|
|
57
|
+
clean_parameters.delete(parameter_element)
|
58
|
+
end
|
59
|
+
clean_parameters
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
require 'svelte/model_factory/parameter'
|
2
|
+
|
3
|
+
module Svelte
|
4
|
+
# Bridging the gap between an application and any external service that
|
5
|
+
# publishes its API as a Swagger JSON spec.
|
6
|
+
# @note This module is supposed to be extended not used directly
|
7
|
+
module ModelFactory
|
8
|
+
# Creates typed Ruby objects from JSON definitions. These definitions are
|
9
|
+
# found in the Swagger JSON spec as a top-level key, "definitions".
|
10
|
+
# @param json [Hash] hash of a swagger models definition
|
11
|
+
# @return [Hash] A hash of model names to models created
|
12
|
+
def define_models(json)
|
13
|
+
return unless json
|
14
|
+
models = {}
|
15
|
+
model_definitions = json['definitions']
|
16
|
+
model_definitions.each do |model_name, parameters|
|
17
|
+
attributes = parameters['properties'].keys
|
18
|
+
model = Class.new do
|
19
|
+
attr_reader(*attributes.map(&:to_sym))
|
20
|
+
|
21
|
+
parameters['properties'].each do |attribute, options|
|
22
|
+
define_method("#{attribute}=", lambda do |value|
|
23
|
+
if public_send(attribute).nil? || !public_send(attribute).present?
|
24
|
+
permitted_values = options.fetch('enum', [])
|
25
|
+
required = parameters.fetch('required', []).include?(attribute)
|
26
|
+
instance_variable_set(
|
27
|
+
"@#{attribute}",
|
28
|
+
Parameter.new(options['type'],
|
29
|
+
permitted_values: permitted_values,
|
30
|
+
required: required)
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
instance_variable_get("@#{attribute}").value = value
|
35
|
+
end)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
define_initialize_on(model: model)
|
40
|
+
define_attributes_on(model: model)
|
41
|
+
define_required_attributes_on(model: model)
|
42
|
+
define_json_for_model_on(model: model)
|
43
|
+
define_nested_models_on(model: model)
|
44
|
+
define_as_json_on(model: model)
|
45
|
+
define_to_json_on(model: model)
|
46
|
+
define_validate_on(model: model)
|
47
|
+
define_valid_on(model: model)
|
48
|
+
|
49
|
+
model.instance_variable_set('@json_for_model', parameters.freeze)
|
50
|
+
|
51
|
+
models[model_name] = model
|
52
|
+
end
|
53
|
+
|
54
|
+
models.each do |model_name, model|
|
55
|
+
const_set(model_name, model)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# Creates typed Ruby objects from JSON String definitions. These definitions
|
60
|
+
# are found in the Swagger JSON spec as a top-level key, "models".
|
61
|
+
# @param string [String] string of json for a swagger models definition
|
62
|
+
# @return [Hash] A hash of model names to models created
|
63
|
+
def define_models_from_json_string(string)
|
64
|
+
define_models(JSON.parse(string)) if string
|
65
|
+
end
|
66
|
+
|
67
|
+
# Creates typed Ruby objects from JSON File definitions. These definitions
|
68
|
+
# are found in the Swagger JSON spec as a top-level key, "models".
|
69
|
+
# @param file [String] path to a json file for a swagger models definition
|
70
|
+
# @return [Hash] A hash of model names to models created
|
71
|
+
def define_models_from_file(file)
|
72
|
+
define_models_from_json_string(File.read(file)) if file
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def define_initialize_on(model:)
|
78
|
+
model.send(:define_method, :initialize, lambda do
|
79
|
+
(required_attributes - nested_models.keys).each do |required|
|
80
|
+
public_send("#{required}=", Parameter::UNSET)
|
81
|
+
end
|
82
|
+
required_nested_models = nested_models.keys & required_attributes
|
83
|
+
nested_models.each do |nested_model_name, nested_model_info|
|
84
|
+
return unless required_nested_models.include?(nested_model_name)
|
85
|
+
nested_class_name = public_send("#{nested_model_name}=",
|
86
|
+
nested_model_info['$ref']
|
87
|
+
.split('/').last)
|
88
|
+
nested_class_name = self.class.name.gsub(/::[^:]*\z/, '::') +
|
89
|
+
nested_class_name
|
90
|
+
public_send("#{nested_model_name}=",
|
91
|
+
Object.const_get(nested_class_name).new)
|
92
|
+
end
|
93
|
+
end)
|
94
|
+
end
|
95
|
+
|
96
|
+
def define_validate_on(model:)
|
97
|
+
model.send(:define_method, :validate, lambda do
|
98
|
+
invalid_params = {}
|
99
|
+
attributes.each do |attribute|
|
100
|
+
if public_send(attribute).respond_to?(:validate)
|
101
|
+
result = public_send(attribute).validate
|
102
|
+
invalid_params[attribute] = result unless result.empty?
|
103
|
+
end
|
104
|
+
end
|
105
|
+
invalid_params
|
106
|
+
end)
|
107
|
+
end
|
108
|
+
|
109
|
+
def define_valid_on(model:)
|
110
|
+
model.send(:define_method, :valid?, lambda do
|
111
|
+
validate.empty?
|
112
|
+
end)
|
113
|
+
end
|
114
|
+
|
115
|
+
def define_to_json_on(model:)
|
116
|
+
model.send(:define_method, :to_json, lambda do
|
117
|
+
as_json.to_json
|
118
|
+
end)
|
119
|
+
end
|
120
|
+
|
121
|
+
def define_attributes_on(model:)
|
122
|
+
model.send(:define_method, :attributes, lambda do
|
123
|
+
@attributes ||= json_for_model['properties'].keys
|
124
|
+
end)
|
125
|
+
end
|
126
|
+
|
127
|
+
def define_as_json_on(model:)
|
128
|
+
model.send(:define_method, :as_json, lambda do
|
129
|
+
structure = {}
|
130
|
+
attributes.each do |attribute|
|
131
|
+
value = if public_send(attribute).respond_to?(:as_json)
|
132
|
+
public_send(attribute).as_json
|
133
|
+
else
|
134
|
+
public_send(attribute)
|
135
|
+
end
|
136
|
+
|
137
|
+
structure[attribute] = value unless value.nil?
|
138
|
+
end
|
139
|
+
|
140
|
+
if structure.empty?
|
141
|
+
nil
|
142
|
+
else
|
143
|
+
symbolised_structure = {}
|
144
|
+
structure.each do |key, value|
|
145
|
+
symbolised_structure[key.to_sym] = value
|
146
|
+
end
|
147
|
+
symbolised_structure
|
148
|
+
end
|
149
|
+
end)
|
150
|
+
end
|
151
|
+
|
152
|
+
def define_json_for_model_on(model:)
|
153
|
+
model.send(:define_method, :json_for_model, lambda do
|
154
|
+
self.class.instance_variable_get('@json_for_model')
|
155
|
+
end)
|
156
|
+
end
|
157
|
+
|
158
|
+
def define_nested_models_on(model:)
|
159
|
+
model.send(:define_method, :nested_models, lambda do
|
160
|
+
json_for_model['properties']
|
161
|
+
.select { |_property, sub_properties| sub_properties.key?('$ref') }
|
162
|
+
end)
|
163
|
+
end
|
164
|
+
|
165
|
+
def define_required_attributes_on(model:)
|
166
|
+
model.send(:define_method, :required_attributes, lambda do
|
167
|
+
@required_attributes ||= json_for_model.fetch('required', [])
|
168
|
+
end)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
module Svelte
|
2
|
+
module ModelFactory
|
3
|
+
# Helper class to wrap around all parameters
|
4
|
+
class Parameter
|
5
|
+
|
6
|
+
# Constant to represent an unset parameter
|
7
|
+
UNSET = Class.new
|
8
|
+
|
9
|
+
# Override of the `inspect` method to return a string representation
|
10
|
+
# of the class
|
11
|
+
def UNSET.inspect
|
12
|
+
'unset'
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :type
|
16
|
+
attr_accessor :value
|
17
|
+
|
18
|
+
# Creates a new Parameter
|
19
|
+
# @param type [String]: Type of the parameter, i.e. `'integer'`
|
20
|
+
# @param permitted_values [Array]: array of allowed values
|
21
|
+
# for the parameter
|
22
|
+
# @param required [Boolean]: is the parameter required?
|
23
|
+
def initialize(type, permitted_values: [], required: false)
|
24
|
+
@type = type
|
25
|
+
@permitted_values = permitted_values
|
26
|
+
@required = required
|
27
|
+
@value = UNSET
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [Boolean] true if and only if the parameter is valid
|
31
|
+
def valid?
|
32
|
+
validate.empty?
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [String] String representing
|
36
|
+
# the validation errors of the parameter
|
37
|
+
def validate
|
38
|
+
# We are not a required parameter, so being unset is fine.
|
39
|
+
return '' if validate_blank
|
40
|
+
|
41
|
+
# if we have a nested model
|
42
|
+
return value.validate if value.respond_to?(:validate)
|
43
|
+
|
44
|
+
messages = validate_messages
|
45
|
+
messages.any? ? 'Invalid parameter: ' + messages.join(', ') : ''
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [Boolean] true if and only if the parameter has been set
|
49
|
+
def present?
|
50
|
+
!unset?
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [Hash] json representation of the parameter
|
54
|
+
def as_json
|
55
|
+
value.respond_to?(:as_json) ? value.as_json : value if present?
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def validate_messages
|
61
|
+
messages = []
|
62
|
+
|
63
|
+
validate_type(messages)
|
64
|
+
|
65
|
+
messages << invalid_type_enum_message unless validate_value_in_enum
|
66
|
+
messages << required_parameter_missing_message unless validate_required
|
67
|
+
|
68
|
+
messages
|
69
|
+
end
|
70
|
+
|
71
|
+
def unset?
|
72
|
+
value == UNSET
|
73
|
+
end
|
74
|
+
|
75
|
+
def validate_blank
|
76
|
+
if @required
|
77
|
+
false
|
78
|
+
else
|
79
|
+
unset?
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def validate_required
|
84
|
+
return true unless @required
|
85
|
+
return false if unset?
|
86
|
+
true
|
87
|
+
end
|
88
|
+
|
89
|
+
def validate_value_in_enum
|
90
|
+
return true if @permitted_values.empty?
|
91
|
+
@permitted_values.include?(value)
|
92
|
+
end
|
93
|
+
|
94
|
+
def validate_type(messages)
|
95
|
+
return true if unset? || not_required_but_nil?
|
96
|
+
|
97
|
+
# TODO: this smells, should we have a duck type that responds
|
98
|
+
# to .validate?
|
99
|
+
valid = case type
|
100
|
+
when 'string'
|
101
|
+
validate_string
|
102
|
+
when 'boolean'
|
103
|
+
validate_boolean
|
104
|
+
when 'number', 'integer'
|
105
|
+
validate_number
|
106
|
+
when 'array'
|
107
|
+
validate_array
|
108
|
+
when 'object'
|
109
|
+
# Objects cannot be validated
|
110
|
+
true
|
111
|
+
else
|
112
|
+
false
|
113
|
+
end
|
114
|
+
messages << invalid_type_message unless valid
|
115
|
+
end
|
116
|
+
|
117
|
+
def invalid_type_message
|
118
|
+
"Expected valid #{type}, but was #{value.inspect}"
|
119
|
+
end
|
120
|
+
|
121
|
+
def invalid_type_enum_message
|
122
|
+
"Expected one of #{@permitted_values.inspect}, but was #{value.inspect}"
|
123
|
+
end
|
124
|
+
|
125
|
+
def required_parameter_missing_message
|
126
|
+
'Missing required parameter'
|
127
|
+
end
|
128
|
+
|
129
|
+
def not_required_but_nil?
|
130
|
+
!@required && value.nil?
|
131
|
+
end
|
132
|
+
|
133
|
+
def validate_string
|
134
|
+
value.is_a?(String)
|
135
|
+
end
|
136
|
+
|
137
|
+
def validate_array_contents
|
138
|
+
value.all? { |v| !v.respond_to?(:valid?) || v.valid? }
|
139
|
+
end
|
140
|
+
|
141
|
+
def validate_array
|
142
|
+
value.is_a?(Array) && validate_array_contents
|
143
|
+
end
|
144
|
+
|
145
|
+
def validate_boolean
|
146
|
+
value == !!value
|
147
|
+
end
|
148
|
+
|
149
|
+
def validate_number
|
150
|
+
value.is_a?(Numeric)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Svelte
|
2
|
+
# Describes a Swagger API Operation
|
3
|
+
class Operation
|
4
|
+
attr_reader :verb, :properties, :path
|
5
|
+
|
6
|
+
# Creates a new Operation.
|
7
|
+
# @param verb [String] operation verb i.e. `'get'`
|
8
|
+
# @param properties [Hash] definition
|
9
|
+
# @param path [Path] Path the operation belongs to
|
10
|
+
def initialize(verb:, properties:, path:)
|
11
|
+
@verb = verb
|
12
|
+
@properties = properties
|
13
|
+
@path = path
|
14
|
+
validate
|
15
|
+
end
|
16
|
+
|
17
|
+
# Operation identifier
|
18
|
+
# @return [String] unique Swagger API operation identifier
|
19
|
+
def id
|
20
|
+
properties['operationId']
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def validate
|
26
|
+
unless id.is_a?(String)
|
27
|
+
raise JSONError, 'Operation is missing mandatory `operationId` field'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Svelte
|
2
|
+
# Dynamically builds Swagger API operations on top of a given module
|
3
|
+
class OperationBuilder
|
4
|
+
class << self
|
5
|
+
# Builds an operation on top of `module_constant`
|
6
|
+
# @param operation [Svete::Operation] operation to build
|
7
|
+
# @param module_constant [Module] operation to build
|
8
|
+
# @param configuration [Configuration] Swagger API configuration
|
9
|
+
def build(operation:, module_constant:, configuration:)
|
10
|
+
builder = self
|
11
|
+
method_name = StringManipulator.method_name_for(operation.id)
|
12
|
+
module_constant.define_singleton_method(method_name) do |*parameters|
|
13
|
+
GenericOperation.call(
|
14
|
+
verb: operation.verb,
|
15
|
+
path: operation.path,
|
16
|
+
configuration: configuration,
|
17
|
+
parameters: builder.request_parameters(full_parameters: parameters),
|
18
|
+
options: builder.options(full_parameters: parameters))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Returns the parameters that are to be sent as part of the request
|
23
|
+
# to the API endpoint.
|
24
|
+
# @param full_parameters [Array] array with the arguments passed into the
|
25
|
+
# method call
|
26
|
+
# @return [Hash] Hash with all the parameters to be sent as part of the
|
27
|
+
# request
|
28
|
+
# @note All keys will be transformed from `Symbol` to `String`
|
29
|
+
def request_parameters(full_parameters:)
|
30
|
+
return {} if full_parameters.compact.empty?
|
31
|
+
full_parameters.first.inject({}) do |memo, (k, v)|
|
32
|
+
memo.merge!(k.to_s => v)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns the options that are to be sent as part of the request
|
37
|
+
# to the API endpoint.
|
38
|
+
# @param full_parameters [Array] array with the arguments passed into the
|
39
|
+
# method call
|
40
|
+
# @return [Hash] Hash with all the options to be sent as part of the
|
41
|
+
# request
|
42
|
+
def options(full_parameters:)
|
43
|
+
full_parameters[1] || {}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|