bluepine 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/bluepine.rb +35 -0
- data/lib/bluepine/assertions.rb +80 -0
- data/lib/bluepine/attributes.rb +92 -0
- data/lib/bluepine/attributes/array_attribute.rb +11 -0
- data/lib/bluepine/attributes/attribute.rb +130 -0
- data/lib/bluepine/attributes/boolean_attribute.rb +22 -0
- data/lib/bluepine/attributes/currency_attribute.rb +19 -0
- data/lib/bluepine/attributes/date_attribute.rb +11 -0
- data/lib/bluepine/attributes/float_attribute.rb +13 -0
- data/lib/bluepine/attributes/integer_attribute.rb +14 -0
- data/lib/bluepine/attributes/ip_address_attribute.rb +15 -0
- data/lib/bluepine/attributes/number_attribute.rb +24 -0
- data/lib/bluepine/attributes/object_attribute.rb +71 -0
- data/lib/bluepine/attributes/schema_attribute.rb +23 -0
- data/lib/bluepine/attributes/string_attribute.rb +36 -0
- data/lib/bluepine/attributes/time_attribute.rb +19 -0
- data/lib/bluepine/attributes/uri_attribute.rb +19 -0
- data/lib/bluepine/attributes/visitor.rb +136 -0
- data/lib/bluepine/endpoint.rb +102 -0
- data/lib/bluepine/endpoints/method.rb +90 -0
- data/lib/bluepine/endpoints/params.rb +115 -0
- data/lib/bluepine/error.rb +17 -0
- data/lib/bluepine/functions.rb +49 -0
- data/lib/bluepine/generators.rb +3 -0
- data/lib/bluepine/generators/generator.rb +16 -0
- data/lib/bluepine/generators/grpc/generator.rb +10 -0
- data/lib/bluepine/generators/open_api/generator.rb +205 -0
- data/lib/bluepine/generators/open_api/property_generator.rb +111 -0
- data/lib/bluepine/registry.rb +75 -0
- data/lib/bluepine/resolvable.rb +11 -0
- data/lib/bluepine/resolver.rb +99 -0
- data/lib/bluepine/serializer.rb +125 -0
- data/lib/bluepine/serializers/serializable.rb +25 -0
- data/lib/bluepine/validator.rb +205 -0
- data/lib/bluepine/validators/normalizable.rb +25 -0
- data/lib/bluepine/validators/proxy.rb +77 -0
- data/lib/bluepine/validators/validatable.rb +48 -0
- data/lib/bluepine/version.rb +3 -0
- metadata +208 -0
@@ -0,0 +1,15 @@
|
|
1
|
+
require "bluepine/attributes/attribute"
|
2
|
+
|
3
|
+
module Bluepine
|
4
|
+
module Attributes
|
5
|
+
class IPAddressAttribute < StringAttribute
|
6
|
+
def spec
|
7
|
+
"RFC 2673 § 3.2"
|
8
|
+
end
|
9
|
+
|
10
|
+
def spec_uri
|
11
|
+
"https://tools.ietf.org/html/rfc2673#section-3.2"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require "bluepine/attributes/attribute"
|
2
|
+
|
3
|
+
module Bluepine
|
4
|
+
module Attributes
|
5
|
+
class NumberAttribute < Attribute
|
6
|
+
self.serializer = ->(v) { v.to_s.include?(".") ? v.to_f : v.to_i }
|
7
|
+
|
8
|
+
RULES = {
|
9
|
+
max: {
|
10
|
+
group: :numericality,
|
11
|
+
name: :less_than_or_equal,
|
12
|
+
},
|
13
|
+
min: {
|
14
|
+
group: :numericality,
|
15
|
+
name: :greater_than_or_equal,
|
16
|
+
},
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
def native_type
|
20
|
+
"number"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require "bluepine/attributes/attribute"
|
2
|
+
|
3
|
+
module Bluepine
|
4
|
+
module Attributes
|
5
|
+
class ObjectAttribute < Attribute
|
6
|
+
class_attribute :stacks
|
7
|
+
attr_reader :attributes
|
8
|
+
|
9
|
+
self.stacks = []
|
10
|
+
|
11
|
+
def initialize(name, options = {}, &block)
|
12
|
+
@name = name
|
13
|
+
@options = options
|
14
|
+
@attributes = {}
|
15
|
+
instance_exec(&block) if block_given?
|
16
|
+
end
|
17
|
+
|
18
|
+
def native_type
|
19
|
+
"object"
|
20
|
+
end
|
21
|
+
|
22
|
+
# Apply default options to all attributes
|
23
|
+
#
|
24
|
+
# group if: :deleted? { ... }
|
25
|
+
# group unless: :deleted? { ... }
|
26
|
+
# group if: ->{ @user.deleted? } { ... }
|
27
|
+
def group(options, &block)
|
28
|
+
return unless block_given?
|
29
|
+
|
30
|
+
# Use stacks to allow nested conditions
|
31
|
+
self.class.stacks << Attribute.options
|
32
|
+
Attribute.options = options
|
33
|
+
|
34
|
+
instance_exec(&block)
|
35
|
+
|
36
|
+
# restore options
|
37
|
+
Attribute.options = self.class.stacks.pop
|
38
|
+
end
|
39
|
+
|
40
|
+
# Shortcut for creating attribute (delegate call to Registry.create)
|
41
|
+
# This allows us to access newly registered attributes
|
42
|
+
#
|
43
|
+
# string :username (or array, number etc)
|
44
|
+
def method_missing(type, name = nil, options = {}, &block)
|
45
|
+
if Attributes.registry.key?(type)
|
46
|
+
@attributes[name] = Attributes.create(type, name, options, &block)
|
47
|
+
else
|
48
|
+
super
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def respond_to_missing?(method, *)
|
53
|
+
super
|
54
|
+
end
|
55
|
+
|
56
|
+
def [](name)
|
57
|
+
@attributes[name.to_sym]
|
58
|
+
end
|
59
|
+
|
60
|
+
def []=(name, attribute)
|
61
|
+
assert_kind_of Attribute, attribute
|
62
|
+
|
63
|
+
@attributes[name.to_sym] = attribute
|
64
|
+
end
|
65
|
+
|
66
|
+
def keys
|
67
|
+
@attributes.keys
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require "bluepine/attributes/attribute"
|
2
|
+
|
3
|
+
module Bluepine
|
4
|
+
module Attributes
|
5
|
+
# Reference to other schema and doesn't accept &block.
|
6
|
+
#
|
7
|
+
# SchemaAttribute supports extra option named `expandable`
|
8
|
+
# which will either return `id` or `serialized object`
|
9
|
+
# as the result.
|
10
|
+
class SchemaAttribute < ObjectAttribute
|
11
|
+
DEFAULT_EXPANDABLE = false
|
12
|
+
def initialize(name, options = {})
|
13
|
+
# automatically add name to :of if it's not given
|
14
|
+
options[:of] = name unless options.key?(:of)
|
15
|
+
@expandable = options.fetch(:expandable, DEFAULT_EXPANDABLE)
|
16
|
+
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :expandable
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require "bluepine/attributes/attribute"
|
2
|
+
|
3
|
+
module Bluepine
|
4
|
+
module Attributes
|
5
|
+
class StringAttribute < Attribute
|
6
|
+
self.serializer = ->(v) { v.to_s }
|
7
|
+
|
8
|
+
RULES = {
|
9
|
+
match: {
|
10
|
+
group: :format,
|
11
|
+
name: :with,
|
12
|
+
},
|
13
|
+
min: {
|
14
|
+
group: :length,
|
15
|
+
name: :minimum,
|
16
|
+
},
|
17
|
+
max: {
|
18
|
+
group: :length,
|
19
|
+
name: :maximum,
|
20
|
+
},
|
21
|
+
range: {
|
22
|
+
group: :length,
|
23
|
+
name: :in,
|
24
|
+
},
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
def native_type
|
28
|
+
"string"
|
29
|
+
end
|
30
|
+
|
31
|
+
def in
|
32
|
+
super&.map(&:to_s)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "bluepine/attributes/attribute"
|
2
|
+
|
3
|
+
module Bluepine
|
4
|
+
module Attributes
|
5
|
+
class TimeAttribute < StringAttribute
|
6
|
+
def format
|
7
|
+
super || "date-time"
|
8
|
+
end
|
9
|
+
|
10
|
+
def spec
|
11
|
+
"ISO 8601"
|
12
|
+
end
|
13
|
+
|
14
|
+
def spec_uri
|
15
|
+
"https://en.wikipedia.org/wiki/ISO_8601"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "bluepine/attributes/attribute"
|
2
|
+
|
3
|
+
module Bluepine
|
4
|
+
module Attributes
|
5
|
+
class URIAttribute < StringAttribute
|
6
|
+
def format
|
7
|
+
super || type
|
8
|
+
end
|
9
|
+
|
10
|
+
def spec
|
11
|
+
"RFC 3986"
|
12
|
+
end
|
13
|
+
|
14
|
+
def spec_uri
|
15
|
+
"https://tools.ietf.org/html/rfc3986"
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
module Bluepine
|
2
|
+
module Attributes
|
3
|
+
# An abstract Visitor for traversing {Attribute}.
|
4
|
+
#
|
5
|
+
# Sub-classes must implement the methods correspond to {Attribute} classes.
|
6
|
+
#
|
7
|
+
# @example Implements +string+ visitor for {StringAttribute}
|
8
|
+
# class SimpleVisitor < Bluepine::Attributes::Visitor
|
9
|
+
# def visit_string(attribute, *args)
|
10
|
+
# "Hello #{attribute.name}"
|
11
|
+
# end
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# # Usage
|
15
|
+
# username = Attributes.create(:string, :username)
|
16
|
+
# visitor = StringVisitor.new
|
17
|
+
# visitor.visit(username) # => "Hello username"
|
18
|
+
#
|
19
|
+
# @abstract
|
20
|
+
class Visitor
|
21
|
+
include Functions
|
22
|
+
|
23
|
+
MethodNotFound = Bluepine::Error.create("Cannot find method Visitor#%s")
|
24
|
+
|
25
|
+
# Traveres a visitable object and calls corresponding method based-on
|
26
|
+
# sub-classes' impementations.
|
27
|
+
#
|
28
|
+
# @example When +attribute+ is an instance of {Attribute}.
|
29
|
+
# object = Attributes.create(:object, :user) { }
|
30
|
+
# visit(object) # => visit_object
|
31
|
+
#
|
32
|
+
# @example When +attribute+ is a {Symbol}.
|
33
|
+
# visit(:user) # => visit_user or visit_schema(attr, of: :user)
|
34
|
+
#
|
35
|
+
# @param [Attribute|Symbol] The +Attribute+ object or +Symbol+
|
36
|
+
def visit(attribute, *args)
|
37
|
+
method, attribute = find_method!(attribute, *args)
|
38
|
+
|
39
|
+
send(method, attribute, *args)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Performs visitor logic when no corresponding method can be found (Catch-all).
|
43
|
+
def visit_attribute(attribute, options = {})
|
44
|
+
raise NotImplementedError
|
45
|
+
end
|
46
|
+
|
47
|
+
def visit_schema(attribute, options = {})
|
48
|
+
raise NotImplementedError
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# Finds a vistor method.
|
54
|
+
#
|
55
|
+
# @return Array<String, Attribute> Pair of method name and +Attribute+ object
|
56
|
+
#
|
57
|
+
# It'll return a first callable method in the chains or return +nil+ when no methods can be found.
|
58
|
+
# If +attribute+ is a Symbol. It'll stop looking any further.
|
59
|
+
#
|
60
|
+
# find_method(:integer) # => `visit_integer`
|
61
|
+
#
|
62
|
+
# If it's an instance of <tt>Attribute</tt>. It'll look up in ancestors
|
63
|
+
# chain and return the first callable method.
|
64
|
+
#
|
65
|
+
# object = Attributes.create(:object, :name) { ... }
|
66
|
+
# find_method(object) => `visit_object`
|
67
|
+
def find_method(attribute, *args)
|
68
|
+
compose(
|
69
|
+
curry(:resolve_methods),
|
70
|
+
curry(:respond_to_visitor?),
|
71
|
+
curry(:normalize_symbol, attribute, args)
|
72
|
+
).(attribute)
|
73
|
+
end
|
74
|
+
|
75
|
+
def find_method!(attribute, *args)
|
76
|
+
method, attribute = find_method(attribute, *args)
|
77
|
+
|
78
|
+
raise MethodNotFound, normalize_method(attribute) unless method
|
79
|
+
|
80
|
+
[method, attribute]
|
81
|
+
end
|
82
|
+
|
83
|
+
# Returns list of method Symbols from given +Attribute+
|
84
|
+
#
|
85
|
+
# @return [Symbol] Method symbols e.g. [:string, StringAttribute, Attribute]
|
86
|
+
def resolve_methods(attribute)
|
87
|
+
return [attribute] if attribute.kind_of?(Symbol)
|
88
|
+
|
89
|
+
# Finds all ancestors in hierarchy up to `Attribute`
|
90
|
+
parents = attribute.class.ancestors.map(&:to_s)
|
91
|
+
parents.slice(0, parents.index(Attribute.name).to_i + 1)
|
92
|
+
end
|
93
|
+
|
94
|
+
def normalize_method(method)
|
95
|
+
return unless method
|
96
|
+
|
97
|
+
# Cannot use `chomp("Attribute")` here, because the top most class is `Attribute`
|
98
|
+
"visit_" + method.to_s.demodulize.gsub(/(\w+)Attribute/, "\\1").underscore
|
99
|
+
end
|
100
|
+
|
101
|
+
# Creates Attribute from symbol
|
102
|
+
# e.g. :integer to IntegerAttribute
|
103
|
+
def normalize_attribute(attribute, options = {})
|
104
|
+
return attribute unless Attributes.key?(attribute)
|
105
|
+
|
106
|
+
Attributes.create(attribute, attribute.to_s, options)
|
107
|
+
end
|
108
|
+
|
109
|
+
# Finds method from method list that respond_to `visit_{attribute}` call
|
110
|
+
def respond_to_visitor?(methods)
|
111
|
+
methods.find { |m| respond_to?(normalize_method(m)) }
|
112
|
+
end
|
113
|
+
|
114
|
+
def normalize_symbol(attribute, args, method)
|
115
|
+
# When we can't find method e.g. `visit_{method}`
|
116
|
+
# and `attribute` is a symbol (e.g. :user),
|
117
|
+
# we'll enforce the same logic for all visitor sub-classes
|
118
|
+
# by calling `visit_schema` with of: attribute
|
119
|
+
if !method && attribute.kind_of?(Symbol)
|
120
|
+
method, attribute = normalize_schema_symbol(attribute, *args)
|
121
|
+
end
|
122
|
+
|
123
|
+
[
|
124
|
+
normalize_method(method),
|
125
|
+
normalize_attribute(attribute, *args),
|
126
|
+
]
|
127
|
+
end
|
128
|
+
|
129
|
+
def normalize_schema_symbol(schema, *args)
|
130
|
+
args.last[:of] = schema if args.last.is_a?(Hash)
|
131
|
+
|
132
|
+
[:schema, :schema]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
require "bluepine/endpoints/params"
|
2
|
+
require "bluepine/endpoints/method"
|
3
|
+
|
4
|
+
module Bluepine
|
5
|
+
class Endpoint
|
6
|
+
include Bluepine::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 ||= Bluepine::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] = Bluepine::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
|