tomyum 0.1.0.a

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +1106 -0
  3. data/lib/tomyum/assertions.rb +80 -0
  4. data/lib/tomyum/attributes/array.rb +11 -0
  5. data/lib/tomyum/attributes/attribute.rb +130 -0
  6. data/lib/tomyum/attributes/boolean.rb +22 -0
  7. data/lib/tomyum/attributes/currency.rb +19 -0
  8. data/lib/tomyum/attributes/date.rb +11 -0
  9. data/lib/tomyum/attributes/float.rb +13 -0
  10. data/lib/tomyum/attributes/integer.rb +14 -0
  11. data/lib/tomyum/attributes/ip_address.rb +15 -0
  12. data/lib/tomyum/attributes/number.rb +24 -0
  13. data/lib/tomyum/attributes/object.rb +71 -0
  14. data/lib/tomyum/attributes/schema.rb +23 -0
  15. data/lib/tomyum/attributes/string.rb +36 -0
  16. data/lib/tomyum/attributes/time.rb +19 -0
  17. data/lib/tomyum/attributes/uri.rb +19 -0
  18. data/lib/tomyum/attributes/visitor.rb +136 -0
  19. data/lib/tomyum/attributes.rb +92 -0
  20. data/lib/tomyum/endpoint.rb +102 -0
  21. data/lib/tomyum/endpoints/method.rb +90 -0
  22. data/lib/tomyum/endpoints/params.rb +115 -0
  23. data/lib/tomyum/error.rb +17 -0
  24. data/lib/tomyum/functions.rb +49 -0
  25. data/lib/tomyum/generators/generator.rb +16 -0
  26. data/lib/tomyum/generators/grpc/generator.rb +10 -0
  27. data/lib/tomyum/generators/open_api/generator.rb +205 -0
  28. data/lib/tomyum/generators/open_api/property_generator.rb +111 -0
  29. data/lib/tomyum/generators.rb +3 -0
  30. data/lib/tomyum/registry.rb +75 -0
  31. data/lib/tomyum/resolvable.rb +11 -0
  32. data/lib/tomyum/resolver.rb +99 -0
  33. data/lib/tomyum/serializer.rb +125 -0
  34. data/lib/tomyum/serializers/serializable.rb +23 -0
  35. data/lib/tomyum/server/app.rb +33 -0
  36. data/lib/tomyum/server/document.rb +20 -0
  37. data/lib/tomyum/server/documents/redoc.rb +36 -0
  38. data/lib/tomyum/server/documents/swagger.rb +47 -0
  39. data/lib/tomyum/server/routes.rb +0 -0
  40. data/lib/tomyum/support.rb +13 -0
  41. data/lib/tomyum/validator.rb +205 -0
  42. data/lib/tomyum/validators/normalizable.rb +24 -0
  43. data/lib/tomyum/validators/proxy.rb +77 -0
  44. data/lib/tomyum/validators/validatable.rb +48 -0
  45. data/lib/tomyum/version.rb +3 -0
  46. data/lib/tomyum.rb +28 -0
  47. metadata +202 -0
@@ -0,0 +1,80 @@
1
+ module Tomyum
2
+ # Declarative way to deal with errors
3
+ module Assertions
4
+ Error = Class.new(StandardError)
5
+ KeyError = Class.new(Error)
6
+ SubsetError = Class.new(Error)
7
+
8
+ extend self
9
+ def self.included(object)
10
+ object.extend(self)
11
+ end
12
+
13
+ # Usage
14
+ #
15
+ # Use default error message
16
+ #
17
+ # assert valid?
18
+ #
19
+ # Use custom error message
20
+ #
21
+ # assert valid?, "Invalid value"
22
+ #
23
+ # Use custom Error class
24
+ #
25
+ # assert valid?, ValidationError, "Invalid value"
26
+ def assert(object, *msgs)
27
+ raises Error, msgs << "#{object.class} is not a truthy" unless object
28
+ end
29
+
30
+ def assert_not(object, *msgs)
31
+ raises Error, msgs << "#{object.class} is not a falsey" if object
32
+ end
33
+
34
+ def assert_kind_of(classes, object, *msgs)
35
+ classes = normalize_array(classes)
36
+ found = classes.find { |klass| object.kind_of?(klass) }
37
+
38
+ raises Error, msgs << "#{object.class} must be an instance of #{classes.map(&:name).join(', ')}" unless found
39
+ end
40
+
41
+ alias_method :assert_kind_of_either, :assert_kind_of
42
+
43
+ # Usage
44
+ #
45
+ # assert_in ["john", "joe"], "joe"
46
+ # assert_in { amount: 1 }, :amount
47
+ def assert_in(list, value, *msgs)
48
+ raises KeyError, msgs << "#{value.class} - #{value} is not in the #{list.keys}" if list.respond_to?(:key?) && !list.key?(value)
49
+ raises KeyError, msgs << "#{value.class} - #{value} is not in the #{list}" if list.kind_of?(Array) && !list.include?(value)
50
+ end
51
+
52
+ def assert_subset_of(parent, subset, *msgs)
53
+ rest = subset - parent
54
+ raises SubsetError, msgs << "#{rest} are not subset of #{parent}" if rest.present?
55
+ end
56
+
57
+ private
58
+
59
+ def normalize_array(values)
60
+ values.respond_to?(:each) ? values : [values]
61
+ end
62
+
63
+ # allow caller to pass custom Error
64
+ def raises(error, msgs = [])
65
+ error, msg = msgs unless msgs.first.kind_of?(String)
66
+
67
+ # Error class has its own error message and caller
68
+ # doesn't specify custom message
69
+ if msgs.length == 2
70
+ if error.respond_to?(:message)
71
+ msg = error.message
72
+ elsif error.respond_to?(:new)
73
+ msg = error.new&.message
74
+ end
75
+ end
76
+
77
+ raise error, msg || msgs.last
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,11 @@
1
+ require_relative "attribute"
2
+
3
+ module Tomyum
4
+ module Attributes
5
+ class Array < Attribute
6
+ def native_type
7
+ "array"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,130 @@
1
+ module Tomyum
2
+ module Attributes
3
+ # An abstract Attribute based class.
4
+ #
5
+ # @abstract
6
+ class Attribute
7
+ include Tomyum::Assertions
8
+ include Tomyum::Serializers::Serializable
9
+ include Tomyum::Validators::Normalizable
10
+ include Tomyum::Validators::Validatable
11
+
12
+ class_attribute :options
13
+ attr_reader :name
14
+
15
+ # Assigns default class attribute values
16
+ self.options = {}.freeze
17
+
18
+ def initialize(name, options = {})
19
+ @name = name
20
+ @options = self.class.options.merge(options)
21
+ end
22
+
23
+ def options
24
+ @options.merge({
25
+ name: @name,
26
+ match: match,
27
+ method: method,
28
+ type: type,
29
+ native_type: native_type,
30
+ of: of,
31
+ in: send(:in),
32
+ if: @options[:if],
33
+ unless: @options[:unless],
34
+ null: null,
35
+ spec: spec,
36
+ spec_uri: spec_uri,
37
+ format: format,
38
+ private: private,
39
+ deprecated: deprecated,
40
+ required: required,
41
+ default: default,
42
+ description: description,
43
+ attributes: attributes.values&.map(&:options),
44
+ })
45
+ end
46
+
47
+ def type
48
+ self.class.name.demodulize.chomp("Attribute").underscore
49
+ end
50
+
51
+ def match
52
+ @options[:match]
53
+ end
54
+
55
+ def method
56
+ @options[:method] || @name
57
+ end
58
+
59
+ def of
60
+ @options[:of]
61
+ end
62
+
63
+ def in
64
+ @options[:in]
65
+ end
66
+
67
+ def if
68
+ @options[:if]
69
+ end
70
+
71
+ def unless
72
+ @options[:unless]
73
+ end
74
+
75
+ def null
76
+ @options.fetch(:null, false)
77
+ end
78
+
79
+ def native_type
80
+ type
81
+ end
82
+
83
+ def format
84
+ @options[:format]
85
+ end
86
+
87
+ def spec
88
+ nil
89
+ end
90
+
91
+ def spec_uri
92
+ nil
93
+ end
94
+
95
+ def attributes
96
+ {}
97
+ end
98
+
99
+ # deprecated attribute should be listed in schema
100
+ def deprecated
101
+ @options.fetch(:deprecated, false)
102
+ end
103
+
104
+ def private
105
+ @options.fetch(:private, false)
106
+ end
107
+
108
+ def required
109
+ @options.fetch(:required, false)
110
+ end
111
+
112
+ def default
113
+ @options[:default]
114
+ end
115
+
116
+ def description
117
+ @options[:description]
118
+ end
119
+
120
+ # Should not be listed in schema or serialize this attribute
121
+ def serializable?
122
+ !private
123
+ end
124
+
125
+ def value(value)
126
+ value.nil? ? default : value
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,22 @@
1
+ require_relative "attribute"
2
+
3
+ module Tomyum
4
+ module Attributes
5
+ # @example Registers custom normalizer that accepts "on" as truth value
6
+ # Boolean.normalizer = ->(x) { ["on", true].include?(x) ? true : false }
7
+ #
8
+ # @example Registers custom serializer
9
+ # Boolean.serialize = ->(x) { x == "on" ? true : false }
10
+ class Boolean < Attribute
11
+ self.serializer = ->(v) { ActiveModel::Type::Boolean.new.cast(v) }
12
+
13
+ def native_type
14
+ "boolean"
15
+ end
16
+
17
+ def in
18
+ @options.fetch(:in, [true, false])
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ require_relative "attribute"
2
+
3
+ module Tomyum
4
+ module Attributes
5
+ class Currency < String
6
+ def format
7
+ super || type
8
+ end
9
+
10
+ def spec
11
+ "ISO 4217"
12
+ end
13
+
14
+ def spec_uri
15
+ "https://en.wikipedia.org/wiki/ISO_4217"
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ require_relative "attribute"
2
+
3
+ module Tomyum
4
+ module Attributes
5
+ class Date < String
6
+ def format
7
+ super || "date"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ require_relative "attribute"
2
+
3
+ module Tomyum
4
+ module Attributes
5
+ class Float < Number
6
+ self.serializer = ->(v) { v.to_f }
7
+
8
+ def format
9
+ super || "float"
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ require_relative "attribute"
2
+
3
+ module Tomyum
4
+ module Attributes
5
+ class Integer < Number
6
+ self.serializer = ->(v) { v.to_i }
7
+
8
+ # JSON schema supports `integer` type
9
+ def native_type
10
+ "integer"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ require_relative "attribute"
2
+
3
+ module Tomyum
4
+ module Attributes
5
+ class IPAddress < String
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_relative "attribute"
2
+
3
+ module Tomyum
4
+ module Attributes
5
+ class Number < 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_relative "attribute"
2
+
3
+ module Tomyum
4
+ module Attributes
5
+ class Object < Attribute
6
+ class_attribute :stacks
7
+ attr_reader :attributes
8
+
9
+ self.stacks = []
10
+
11
+ def initialize(name, options = {}, &block)
12
+ super
13
+
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_relative "attribute"
2
+
3
+ module Tomyum
4
+ module Attributes
5
+ # Reference to other schema and doesn't accept &block.
6
+ #
7
+ # Schema supports extra option named `expandable`
8
+ # which will either return `id` or `serialized object`
9
+ # as the result.
10
+ class Schema < Object
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_relative "attribute"
2
+
3
+ module Tomyum
4
+ module Attributes
5
+ class String < 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_relative "attribute"
2
+
3
+ module Tomyum
4
+ module Attributes
5
+ class Time < String
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_relative "attribute"
2
+
3
+ module Tomyum
4
+ module Attributes
5
+ class URI < String
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 Tomyum
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 {String}
8
+ # class SimpleVisitor < Tomyum::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 = Tomyum::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, String, 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 Integer
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