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