tomyum 0.1.0.a

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