bluepine 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/lib/bluepine.rb +35 -0
  3. data/lib/bluepine/assertions.rb +80 -0
  4. data/lib/bluepine/attributes.rb +92 -0
  5. data/lib/bluepine/attributes/array_attribute.rb +11 -0
  6. data/lib/bluepine/attributes/attribute.rb +130 -0
  7. data/lib/bluepine/attributes/boolean_attribute.rb +22 -0
  8. data/lib/bluepine/attributes/currency_attribute.rb +19 -0
  9. data/lib/bluepine/attributes/date_attribute.rb +11 -0
  10. data/lib/bluepine/attributes/float_attribute.rb +13 -0
  11. data/lib/bluepine/attributes/integer_attribute.rb +14 -0
  12. data/lib/bluepine/attributes/ip_address_attribute.rb +15 -0
  13. data/lib/bluepine/attributes/number_attribute.rb +24 -0
  14. data/lib/bluepine/attributes/object_attribute.rb +71 -0
  15. data/lib/bluepine/attributes/schema_attribute.rb +23 -0
  16. data/lib/bluepine/attributes/string_attribute.rb +36 -0
  17. data/lib/bluepine/attributes/time_attribute.rb +19 -0
  18. data/lib/bluepine/attributes/uri_attribute.rb +19 -0
  19. data/lib/bluepine/attributes/visitor.rb +136 -0
  20. data/lib/bluepine/endpoint.rb +102 -0
  21. data/lib/bluepine/endpoints/method.rb +90 -0
  22. data/lib/bluepine/endpoints/params.rb +115 -0
  23. data/lib/bluepine/error.rb +17 -0
  24. data/lib/bluepine/functions.rb +49 -0
  25. data/lib/bluepine/generators.rb +3 -0
  26. data/lib/bluepine/generators/generator.rb +16 -0
  27. data/lib/bluepine/generators/grpc/generator.rb +10 -0
  28. data/lib/bluepine/generators/open_api/generator.rb +205 -0
  29. data/lib/bluepine/generators/open_api/property_generator.rb +111 -0
  30. data/lib/bluepine/registry.rb +75 -0
  31. data/lib/bluepine/resolvable.rb +11 -0
  32. data/lib/bluepine/resolver.rb +99 -0
  33. data/lib/bluepine/serializer.rb +125 -0
  34. data/lib/bluepine/serializers/serializable.rb +25 -0
  35. data/lib/bluepine/validator.rb +205 -0
  36. data/lib/bluepine/validators/normalizable.rb +25 -0
  37. data/lib/bluepine/validators/proxy.rb +77 -0
  38. data/lib/bluepine/validators/validatable.rb +48 -0
  39. data/lib/bluepine/version.rb +3 -0
  40. 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