bluepine 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/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
|