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,111 @@
1
+ module Tomyum
2
+ module Generators
3
+ module OpenAPI
4
+ # Generate property based on Open API Spec
5
+ class PropertyGenerator < Tomyum::Attributes::Visitor
6
+ include Tomyum::Assertions
7
+
8
+ class << self
9
+ def visit(attr, options = {})
10
+ new.visit(attr, options)
11
+ end
12
+
13
+ alias_method :generate, :visit
14
+ end
15
+
16
+ def visit(attr, options = {})
17
+ attr = normalize_attribute(attr, options)
18
+
19
+ # handle case when attr is a Symbol (reference)
20
+ return attr unless attr.respond_to?(:native_type)
21
+
22
+ super
23
+ end
24
+
25
+ # catch-all
26
+ def visit_attribute(attr, options = {})
27
+ build(attr, options)
28
+ end
29
+
30
+ def visit_array(attr, options = {})
31
+ build(attr, options).tap do |property|
32
+ property[:items] = attr.of ? visit(attr.of, options) : {}
33
+ end
34
+ end
35
+
36
+ def visit_object(attr, options = {})
37
+ build(attr, options).tap do |property|
38
+ required = []
39
+ attr.attributes.values.each_with_object(property) do |attribute, object|
40
+
41
+ # Adds to required list
42
+ required << attribute.name if attribute.required
43
+
44
+ object[:properties] ||= {}
45
+ object[:properties][attribute.name] = visit(attribute, options) if attribute.serializable?
46
+ end
47
+
48
+ # additional options
49
+ property[:required] = required unless required.empty?
50
+ end
51
+ end
52
+
53
+ # Handle Schema
54
+ def visit_schema(attr, options)
55
+ return build_ref(attr.of) unless attr.expandable
56
+
57
+ # Schema#of may contains array of references
58
+ # e.g. of = [:user, :customer]
59
+ refs = Array(attr.of).map { |of| build_ref(of) }
60
+ refs << visit("string")
61
+
62
+ {
63
+ "oneOf": refs,
64
+ }
65
+ end
66
+
67
+ def normalize_attribute(object, options = {})
68
+ return build_ref(object, options) if object.kind_of?(Symbol)
69
+ return object if object.respond_to?(:native_type)
70
+
71
+ # object is string (native types e.g. "integer", "boolean" etc)
72
+ Tomyum::Attributes.create(object.to_sym, object)
73
+ end
74
+
75
+ private
76
+
77
+ def build(attr, options = {})
78
+ assert_kind_of Tomyum::Attributes::Attribute, attr
79
+
80
+ # build base property
81
+ {
82
+ type: attr.native_type,
83
+ }.tap do |property|
84
+ property[:description] = attr.description if attr.description.present?
85
+ property[:default] = attr.default if attr.default
86
+ property[:enum] = attr.in if attr.in
87
+ property[:nullable] = attr.null if attr.null
88
+ property[:format] = attr.format if attr.format
89
+ property[:pattern] = build_pattern(attr.match) if attr.match
90
+ property["x-custom-schema"] = options[:schema] if options[:schema].present?
91
+ end
92
+ end
93
+
94
+ # create $ref
95
+ def build_ref(attr, options = {})
96
+ ref = options[:as] || attr
97
+
98
+ {
99
+ "$ref": "#/components/schemas/#{ref}",
100
+ }
101
+ end
102
+
103
+ def build_pattern(value)
104
+ return value.source if value.respond_to?(:source)
105
+
106
+ value
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,3 @@
1
+ require_relative "generators/generator"
2
+ require_relative "generators/open_api/generator"
3
+ require_relative "generators/open_api/property_generator"
@@ -0,0 +1,75 @@
1
+ module Tomyum
2
+ # A generic registry
3
+ #
4
+ # @example Create key/value pairs registry (Hash)
5
+ # registry = Registry.new do |key, value|
6
+ # { name: key, age: value }
7
+ # end
8
+ #
9
+ # # Register object
10
+ # registry.register(:john, name: :john, age: 10)
11
+ #
12
+ # # Retrieve object
13
+ # registry.get(:john) # => { name: :john, age: 10 }
14
+ #
15
+ # # Create new object
16
+ # registry.create(:joe, 10) # => { name: joe, age: 10 }
17
+ class Registry
18
+ include Tomyum::Assertions
19
+
20
+ KeyError = Tomyum::Error.create("Object %s already exists")
21
+
22
+ # @param [Object] A collection of objects which has #name property
23
+ # @param &block A {Proc} that'll create new object
24
+ def initialize(objects = [], error: KeyError, &block)
25
+ assert_kind_of Proc, block
26
+
27
+ @objects = normalize objects
28
+ @factory = block
29
+ @error = error
30
+ end
31
+
32
+ # Registers new object by id
33
+ #
34
+ # @param id [String] Unique name
35
+ # @param object [Object] Object to register
36
+ # @param override [Boolean] Overrides existing key if exists
37
+ def register(id, object, override: false)
38
+ if key?(id) && !override
39
+ raise @error, id
40
+ end
41
+
42
+ @objects[id.to_sym] = object
43
+ end
44
+
45
+ # Creates new object by using a {Proc} from #new
46
+ #
47
+ # @return [Object]
48
+ # @example
49
+ # registry.create(:user, "john")
50
+ def create(id, *args, &block)
51
+ instance_exec(id, *args, block, &@factory)
52
+ end
53
+
54
+ # Retrieves registered Object by key
55
+ def get(id)
56
+ raise @error, id unless key?(id)
57
+
58
+ @objects[id.to_sym]
59
+ end
60
+
61
+ def key?(id)
62
+ @objects.key? id
63
+ end
64
+
65
+ def keys
66
+ @objects.keys
67
+ end
68
+
69
+ private
70
+
71
+ def normalize(objects = [])
72
+ (objects || []).each_with_object({}) { |object, target| target[object.name] = object }
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,11 @@
1
+ module Tomyum
2
+ module Resolvable
3
+ ResolverRequired = Tomyum::Error.create("Resolver is required")
4
+
5
+ def resolver
6
+ raise ResolverRequired unless @resolver
7
+
8
+ @resolver
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,99 @@
1
+ module Tomyum
2
+ # Responsible for registering and looking up the schemas and endpoints
3
+ #
4
+ # @example Register via +:schemas+ and +:endpoints+ options
5
+ # resolver = Resolver.new(schemas: [], endpoints: [])
6
+ #
7
+ # @example Register via +block+
8
+ # resolver = Resolver.new do
9
+ # schema :user do
10
+ # string :username
11
+ # end
12
+ #
13
+ # schema :team do
14
+ # string :name
15
+ # end
16
+ #
17
+ # endpoint "/users" do
18
+ # string :username
19
+ # end
20
+ # end
21
+ #
22
+ # @example Manually register new schema/endpoint
23
+ # resolver.schema(:user) do
24
+ # string :username
25
+ # end
26
+ #
27
+ # resolver.endpoint("/teams") do
28
+ # post :create
29
+ # end
30
+ #
31
+ # @example Register an existing schema/endpoint
32
+ # resolver.schemas.register(:user, user_schema)
33
+ #
34
+ class Resolver
35
+ Endpoint = Tomyum::Endpoint
36
+ Attributes = Tomyum::Attributes
37
+
38
+ SchemaNotFound = Tomyum::Error.create("Endpoint %s cannot be found")
39
+ EndpointNotFound = Tomyum::Error.create("Schema %s cannot be found")
40
+
41
+ def initialize(schemas: [], endpoints: [], schema_registry: nil, endpoint_registry: nil, &block)
42
+ @registries = {
43
+ schemas: create_schema_registry(schemas, schema_registry),
44
+ endpoints: create_endpoint_registry(endpoints, endpoint_registry)
45
+ }
46
+
47
+ instance_exec(&block) if block_given?
48
+ end
49
+
50
+ def resolve(type, name)
51
+ @registries[type].get(name)
52
+ end
53
+
54
+ def register(type, name, args, &block)
55
+ @registries[type].create(name, **args, &block)
56
+ end
57
+
58
+ # Exposes schema registry
59
+ def schemas
60
+ @registries[:schemas]
61
+ end
62
+
63
+ # Exposes endpoint registry
64
+ def endpoints
65
+ @registries[:endpoints]
66
+ end
67
+
68
+ def schema(name, options = {}, &block)
69
+ return resolve(:schemas, name) unless block_given?
70
+
71
+ register(:schemas, name, options, &block)
72
+ end
73
+
74
+ def endpoint(path, options = {}, &block)
75
+ return resolve(:endpoints, Endpoint.normalize_name(path)) unless block_given?
76
+
77
+ register(:endpoints, path, options, &block)
78
+ end
79
+
80
+ private
81
+
82
+ def create_schema_registry(schemas, registry = nil)
83
+ return registry.(schemas) if registry
84
+
85
+ Registry.new(schemas, error: SchemaNotFound) do |name, options = {}, block|
86
+ @objects[name] = Attributes.create(:object, name, options, &block)
87
+ end
88
+ end
89
+
90
+ def create_endpoint_registry(endpoints, registry = nil)
91
+ return registry.(endpoints) if registry
92
+
93
+ Registry.new(endpoints, error: EndpointNotFound) do |path, options = {}, block|
94
+ endpoint = Endpoint.new(path, options, &block)
95
+ @objects[endpoint.name] = endpoint
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,125 @@
1
+ module Tomyum
2
+ # user_schema = Attributes.create(:object) do
3
+ # string :username
4
+ # string :email
5
+ # end
6
+ #
7
+ # user = <any object>
8
+ #
9
+ # resolver = Resolver.new(schemas: [user_schema, ...])
10
+ # serializer.serialize(user_schema, user)
11
+ class Serializer < Tomyum::Attributes::Visitor
12
+ include Tomyum::Resolvable
13
+
14
+ InvalidPredicate = Tomyum::Error.create("Invalid predicate value (must be either Symbol or Proc)")
15
+
16
+ def initialize(resolver = nil)
17
+ @resolver = resolver
18
+ end
19
+
20
+ # Override to make it accepts 3 arguments
21
+ def normalize_attribute(attribute, object, options = {})
22
+ super(attribute, options)
23
+ end
24
+
25
+ # catch all
26
+ def visit_attribute(attribute, object, options = {})
27
+ attribute.serialize(attribute.value(object))
28
+ end
29
+
30
+ # Primitive attribute
31
+ # -------------------
32
+ # serialize(string, "user")
33
+ # serialize(object, { name: "john" })
34
+ #
35
+ # Schema Attribute
36
+ # ----------------
37
+ # schema = Attributes.create(:object, :user) do
38
+ # string :username
39
+ # end
40
+ #
41
+ # class User
42
+ # def initialize(data)
43
+ # @data = data
44
+ # end
45
+ #
46
+ # def username
47
+ # @data[:name]
48
+ # end
49
+ # end
50
+ # user = User.new(name: "john")
51
+ #
52
+ # serialize(schema, user)
53
+ alias :serialize :visit
54
+
55
+ # Defines visitors for primitive types e.g. `visit_string` etc
56
+ Tomyum::Attributes::SCALAR_TYPES.each do |type|
57
+ alias_method "visit_#{type}", :visit_attribute
58
+ end
59
+
60
+ def visit_object(attribute, object, options = {})
61
+ visit_object_handler(attribute, object) do |attr, value, attrs|
62
+ attrs[attr.name] = visit(attr, value, options)
63
+ end
64
+ end
65
+
66
+ def visit_array(attribute, object, options = {})
67
+ as = attribute.of
68
+
69
+ Array(object).map do |item|
70
+ # item#serialize_as will be used when of: option is not specified.
71
+ # e.g. ListSerializer.schema has `array :data`
72
+ # as = attribute.of || (item.serialize_as if item.respond_to?(:serialize_as))
73
+ unless as.kind_of?(Symbol)
74
+ item
75
+ else
76
+ visit(as, item, options)
77
+ end
78
+ end
79
+ end
80
+
81
+ def visit_schema(attribute, object, options = {})
82
+ attribute = resolver.schema(attribute.of)
83
+
84
+ visit_object(attribute, object, options)
85
+ end
86
+
87
+ private
88
+
89
+ def get(object, name)
90
+ object.respond_to?(name) ? object.send(name) : object&.fetch(name, nil)
91
+ end
92
+
93
+ def visit_object_handler(attribute, object)
94
+ attribute.attributes.values.each_with_object({}) do |attr, attrs|
95
+ next unless serializable?(attr, object)
96
+
97
+ # get value for each field
98
+ value = get(object, attr.method)
99
+
100
+ yield(attr, value, attrs)
101
+ end
102
+ end
103
+
104
+ def serializable?(attr, object)
105
+ return unless attr.serializable?
106
+
107
+ # check predicate :if and :unless
108
+ return execute_predicate(attr.if, object) if attr.if
109
+ return !execute_predicate(attr.unless, object) if attr.unless
110
+
111
+ true
112
+ end
113
+
114
+ def execute_predicate(predicate, object)
115
+ case predicate
116
+ when Symbol
117
+ get(object, predicate)
118
+ when Proc
119
+ predicate.call(object)
120
+ else
121
+ raise InvalidPredicate
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,23 @@
1
+ module Tomyum
2
+ module Serializers
3
+ module Serializable
4
+ extend Tomyum::Support
5
+
6
+ included do
7
+ class << self
8
+ attr_accessor :serializer
9
+
10
+ def serializer
11
+ @serializer || superclass.serializer
12
+ end
13
+ end
14
+
15
+ self.serializer = ->(v) { v }
16
+ end
17
+
18
+ def serialize(value)
19
+ self.class.serializer.(value)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,33 @@
1
+ module Tomyum
2
+ module Server
3
+ class App
4
+ def initialize(resolver = nil)
5
+ @resolver = resolver
6
+ end
7
+
8
+ def call(env)
9
+ resolver = Tomyum::Resolver.new do
10
+ endpoint "/users" do
11
+ get :create, path: "/"
12
+ end
13
+ end
14
+
15
+ body = ['<h1>Hello</h1>']
16
+ resolver.endpoints.keys.each do |key|
17
+ endpoint = resolver.endpoints.get(key)
18
+ methods = endpoint.methods
19
+ methods.map do |name, method|
20
+ path = [endpoint.path, method.path].map { |r| r.chomp '/' }.reject(&:empty?).join("/")
21
+ if env["REQUEST_PATH"] == path && env["REQUEST_METHOD"].downcase == method.verb.to_s
22
+ body = ["Path #{path}"]
23
+ end
24
+ end
25
+ end
26
+
27
+ status = 200
28
+ headers = { "Content-Type" => "text/html" }
29
+ [status, headers, body]
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,20 @@
1
+ require_relative "documents/swagger"
2
+ require_relative "documents/redoc"
3
+
4
+ module Tomyum
5
+ module Server
6
+ class Document
7
+ def initialize(app, path: "/doc")
8
+ @app = app
9
+ @path = path
10
+ end
11
+
12
+ def call(env)
13
+ request = Rack::Request.new(env)
14
+ return @app.call(env) if request.path != @path
15
+
16
+ [200, {}, [Documents::Redoc.new.call]]
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,36 @@
1
+ module Tomyum
2
+ module Server
3
+ module Documents
4
+ class Redoc
5
+ def call
6
+ <<~HTML
7
+ <!DOCTYPE html>
8
+ <html>
9
+ <head>
10
+ <title>Redoc</title>
11
+ <!-- needed for adaptive design -->
12
+ <meta charset="utf-8"/>
13
+ <meta name="viewport" content="width=device-width, initial-scale=1">
14
+ <link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
15
+
16
+ <!--
17
+ Redoc doesn't change outer page styles
18
+ -->
19
+ <style>
20
+ body {
21
+ margin: 0;
22
+ padding: 0;
23
+ }
24
+ </style>
25
+ </head>
26
+ <body>
27
+ <redoc spec-url='http://petstore.swagger.io/v2/swagger.json'></redoc>
28
+ <script src="https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js"> </script>
29
+ </body>
30
+ </html>
31
+ HTML
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,47 @@
1
+ module Tomyum
2
+ module Server
3
+ module Documents
4
+ class Swagger
5
+ def call
6
+ <<~HTML
7
+ <!DOCTYPE html>
8
+ <html xmlns="http://www.w3.org/1999/xhtml">
9
+ <head>
10
+ <meta charset="UTF-8">
11
+ <link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@3.12.1/swagger-ui.css">
12
+ </head>
13
+ <body>
14
+
15
+ <div id="swagger-ui"></div>
16
+
17
+ <script src="https://unpkg.com/swagger-ui-dist@3.12.1/swagger-ui-standalone-preset.js"></script>
18
+ <script src="https://unpkg.com/swagger-ui-dist@3.12.1/swagger-ui-bundle.js"></script>
19
+
20
+ <script>
21
+
22
+ window.onload = function() {
23
+ // Build a system
24
+ const ui = SwaggerUIBundle({
25
+ url: "https://petstore.swagger.io/v2/swagger.json",
26
+ dom_id: '#swagger-ui',
27
+ deepLinking: true,
28
+ presets: [
29
+ SwaggerUIBundle.presets.apis,
30
+ SwaggerUIStandalonePreset
31
+ ],
32
+ // plugins: [
33
+ // SwaggerUIBundle.plugins.DownloadUrl
34
+ // ],
35
+ layout: "StandaloneLayout",
36
+ })
37
+ // window.ui = ui
38
+ }
39
+ </script>
40
+ </body>
41
+ </html>
42
+ HTML
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
File without changes
@@ -0,0 +1,13 @@
1
+ module Tomyum
2
+ module Support
3
+ # Mimic Active Support's .included behaviours
4
+ def included(base = nil, &block)
5
+ if base.nil?
6
+ @_included_block = block
7
+ else
8
+ base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
9
+ base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
10
+ end
11
+ end
12
+ end
13
+ end