litl 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +23 -0
- data/.ruby-version +1 -0
- data/.travis.yml +19 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +89 -0
- data/LICENSE.txt +21 -0
- data/Rakefile +21 -0
- data/bin/console +8 -0
- data/lib/lit.rb +22 -0
- data/lib/lit/builder.rb +12 -0
- data/lib/lit/builder/array.rb +27 -0
- data/lib/lit/builder/map.rb +30 -0
- data/lib/lit/builder/object.rb +175 -0
- data/lib/lit/builder/option.rb +32 -0
- data/lib/lit/errors.rb +11 -0
- data/lib/lit/loader.rb +78 -0
- data/lib/lit/module_observer.rb +21 -0
- data/lib/lit/object.rb +14 -0
- data/lib/lit/object/array.rb +34 -0
- data/lib/lit/object/enum.rb +9 -0
- data/lib/lit/object/enum_variant.rb +9 -0
- data/lib/lit/object/map.rb +29 -0
- data/lib/lit/object/option.rb +39 -0
- data/lib/lit/object/struct.rb +9 -0
- data/lib/lit/request_deserializer.rb +104 -0
- data/lib/lit/serializer.rb +81 -0
- data/lib/lit/type_checker.rb +106 -0
- data/lib/lit/utils.rb +44 -0
- data/lib/lit/version.rb +9 -0
- data/litl.gemspec +38 -0
- metadata +189 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LIT
|
4
|
+
module Builder
|
5
|
+
# @api private
|
6
|
+
# @since 0.1.0
|
7
|
+
class Option
|
8
|
+
def initialize(mod, type)
|
9
|
+
@type_checker = TypeChecker.new(mod)
|
10
|
+
@type = type
|
11
|
+
end
|
12
|
+
|
13
|
+
def build
|
14
|
+
mod = Module.new do
|
15
|
+
include ::LIT::Object::Option
|
16
|
+
end
|
17
|
+
|
18
|
+
type_checker = @type_checker
|
19
|
+
type = @type
|
20
|
+
|
21
|
+
mod::Some.define_method(:check_type!) do |value|
|
22
|
+
# NOTE: actually covered, but SimpleCov cannot detect that
|
23
|
+
# :nocov:
|
24
|
+
type_checker.check_type!(type, value)
|
25
|
+
# :nocov:
|
26
|
+
end
|
27
|
+
|
28
|
+
mod
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/lib/lit/errors.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LIT
|
4
|
+
# @api public
|
5
|
+
# @since 0.1.0
|
6
|
+
TypeError = Class.new(::TypeError)
|
7
|
+
|
8
|
+
InvalidASTError = Class.new(StandardError)
|
9
|
+
InvalidObjectError = Class.new(StandardError)
|
10
|
+
InvalidTypeError = Class.new(StandardError)
|
11
|
+
end
|
data/lib/lit/loader.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LIT
|
4
|
+
# @api public
|
5
|
+
# @since 0.1.0
|
6
|
+
class Loader
|
7
|
+
FILE_EXTENSION = ".lit"
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
loader = self
|
11
|
+
|
12
|
+
@target_module = Module.new do
|
13
|
+
include ::LIT::Object
|
14
|
+
|
15
|
+
define_singleton_method(:reload_file) do |file_path|
|
16
|
+
loader.reload_file(file_path)
|
17
|
+
end
|
18
|
+
|
19
|
+
define_singleton_method(:serialize) do |object|
|
20
|
+
Serializer.new(object).serialize
|
21
|
+
end
|
22
|
+
|
23
|
+
define_singleton_method(:deserialize_request) do |data|
|
24
|
+
RequestDeserializer.new(data, self).deserialize_request
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
@working_module = @target_module
|
29
|
+
end
|
30
|
+
|
31
|
+
def load_directory(dir_path)
|
32
|
+
glob = File.join("**", "*#{FILE_EXTENSION}")
|
33
|
+
@base_dir = Pathname.new(dir_path)
|
34
|
+
|
35
|
+
Dir.glob(glob, base: dir_path) do |filename|
|
36
|
+
load_file(filename)
|
37
|
+
end
|
38
|
+
|
39
|
+
@target_module
|
40
|
+
end
|
41
|
+
|
42
|
+
def reload_file(file_path)
|
43
|
+
@working_module = @target_module
|
44
|
+
load_file(file_path)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def load_file(filename)
|
50
|
+
file_path = Pathname.new(File.expand_path(filename, @base_dir))
|
51
|
+
relative_path = file_path.relative_path_from(@base_dir).to_s
|
52
|
+
namespace = [*File.dirname(relative_path).split(File::SEPARATOR).slice(1),
|
53
|
+
File.basename(relative_path, FILE_EXTENSION)]
|
54
|
+
|
55
|
+
if namespace == %w[main]
|
56
|
+
load_main_file(file_path)
|
57
|
+
else
|
58
|
+
load_regular_file(file_path, namespace)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def load_main_file(file_path)
|
63
|
+
ast = Parser.parse_file(file_path)
|
64
|
+
Builder::Object.new(ast, @target_module).build
|
65
|
+
end
|
66
|
+
|
67
|
+
def load_regular_file(file_path, namespace)
|
68
|
+
namespace.each do |mod|
|
69
|
+
@working_module = Utils.const_reset(@working_module, Utils.camelize(mod), Module.new)
|
70
|
+
end
|
71
|
+
|
72
|
+
ast = Parser.parse_file(file_path)
|
73
|
+
|
74
|
+
Builder::Object.new(ast, @working_module).build
|
75
|
+
@working_module.include(@target_module)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LIT
|
4
|
+
# @api public
|
5
|
+
# @since 0.1.0
|
6
|
+
module ModuleObserver
|
7
|
+
def self.included(object)
|
8
|
+
object.define_singleton_method(:observe) do |target_module|
|
9
|
+
namespace = target_module.name.split("::")
|
10
|
+
|
11
|
+
define_singleton_method(:const_missing) do |const_name|
|
12
|
+
mod = namespace.reduce(self) do |mod, child_name|
|
13
|
+
mod.const_get(child_name)
|
14
|
+
end
|
15
|
+
|
16
|
+
mod.const_get(const_name)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/lit/object.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LIT
|
4
|
+
# @api public
|
5
|
+
# @since 0.1.0
|
6
|
+
module Object
|
7
|
+
require_relative "object/array"
|
8
|
+
require_relative "object/enum"
|
9
|
+
require_relative "object/enum_variant"
|
10
|
+
require_relative "object/map"
|
11
|
+
require_relative "object/option"
|
12
|
+
require_relative "object/struct"
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LIT
|
4
|
+
module Object
|
5
|
+
# @api public
|
6
|
+
# @since 0.1.0
|
7
|
+
class Array
|
8
|
+
def initialize(*values)
|
9
|
+
values.each { |v| check_type!(v) }
|
10
|
+
@values = values
|
11
|
+
end
|
12
|
+
|
13
|
+
def each(&block)
|
14
|
+
@values.each(&block)
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_a
|
18
|
+
@values
|
19
|
+
end
|
20
|
+
|
21
|
+
def [](index)
|
22
|
+
@values[index]
|
23
|
+
end
|
24
|
+
|
25
|
+
def __values__
|
26
|
+
@values
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def check_type!(value); end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LIT
|
4
|
+
module Object
|
5
|
+
# @api public
|
6
|
+
# @since 0.1.0
|
7
|
+
class Map
|
8
|
+
attr_reader :values
|
9
|
+
|
10
|
+
def initialize(values = {})
|
11
|
+
values = Utils::MapHash.new(values)
|
12
|
+
values.each { |k, v| check_type!(k, v) }
|
13
|
+
@values = values
|
14
|
+
end
|
15
|
+
|
16
|
+
def [](key)
|
17
|
+
@values[key]
|
18
|
+
end
|
19
|
+
|
20
|
+
def __values__
|
21
|
+
@values.to_h
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def check_type!(key, value); end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LIT
|
4
|
+
module Object
|
5
|
+
def self.included(object)
|
6
|
+
object.define_singleton_method(:Some) { |value| Option::Some.new(value) }
|
7
|
+
Utils.const_reset(object, "None", Option::None)
|
8
|
+
Utils.const_reset(object, "Some", Class.new(Option::Some))
|
9
|
+
end
|
10
|
+
|
11
|
+
# @api public
|
12
|
+
# @since 0.1.0
|
13
|
+
module Option
|
14
|
+
def self.included(object)
|
15
|
+
object.define_singleton_method(:Some) { |value| Some.new(value) }
|
16
|
+
Utils.const_reset(object, "Some", Class.new(Some))
|
17
|
+
end
|
18
|
+
|
19
|
+
None = Class.new
|
20
|
+
|
21
|
+
class Some
|
22
|
+
attr_reader :value
|
23
|
+
|
24
|
+
def initialize(value)
|
25
|
+
check_type!(value)
|
26
|
+
@value = value
|
27
|
+
end
|
28
|
+
|
29
|
+
def __value__
|
30
|
+
@value
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def check_type!(value); end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LIT
|
4
|
+
# @api public
|
5
|
+
# @since 0.1.0
|
6
|
+
class RequestDeserializer
|
7
|
+
AST = Parser::AST
|
8
|
+
|
9
|
+
def initialize(raw_request, type_root)
|
10
|
+
# { action, payload }
|
11
|
+
@raw_request = raw_request
|
12
|
+
@type_root = type_root
|
13
|
+
end
|
14
|
+
|
15
|
+
def deserialize_request
|
16
|
+
action_namespace = @raw_request.fetch("action").split(".").map { |x| Utils.camelize(x) }
|
17
|
+
action_type = action_namespace.reduce(@type_root) do |object, namespace|
|
18
|
+
object.const_get(namespace)
|
19
|
+
end
|
20
|
+
|
21
|
+
request_type = action_type.const_get("Request")
|
22
|
+
@type_module = action_type.const_get("DefinedIn")
|
23
|
+
|
24
|
+
deserialize(request_type, @raw_request.fetch("payload"))
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# rubocop:disable Metrics/MethodLength
|
30
|
+
def deserialize(type, payload)
|
31
|
+
if type.is_a?(Module)
|
32
|
+
if type <= Object::Struct
|
33
|
+
deserialize_struct(type, payload)
|
34
|
+
elsif type <= Object::Enum
|
35
|
+
deserialize_enum(type, payload)
|
36
|
+
elsif type == AST::Type::Primitive::String ||
|
37
|
+
type == AST::Type::Primitive::Integer ||
|
38
|
+
type == AST::Type::Primitive::Float ||
|
39
|
+
type == AST::Type::Primitive::Boolean
|
40
|
+
payload
|
41
|
+
else
|
42
|
+
deserialize(type::TYPE, payload)
|
43
|
+
end
|
44
|
+
elsif type.is_a?(AST::Type::Alias)
|
45
|
+
deserialize(@type_module.const_get(type.name), payload)
|
46
|
+
elsif type.is_a?(AST::Type::Primitive::Array)
|
47
|
+
deserialize_array(type, payload)
|
48
|
+
elsif type.is_a?(AST::Type::Primitive::Map)
|
49
|
+
deserialize_map(type, payload)
|
50
|
+
elsif type.is_a?(AST::Type::Primitive::Option)
|
51
|
+
deserialize_option(type.type, payload)
|
52
|
+
else
|
53
|
+
raise InvalidTypeError, "invalid type: #{type}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
# rubocop:enable Metrics/MethodLength
|
57
|
+
|
58
|
+
def deserialize_struct(struct_klass, struct_data)
|
59
|
+
params = struct_data.reduce({}) do |acc, (key, value)|
|
60
|
+
field_name = key.to_sym
|
61
|
+
field_type = struct_klass.__fields__[field_name]
|
62
|
+
acc.merge(field_name => deserialize(field_type, value))
|
63
|
+
end
|
64
|
+
|
65
|
+
struct_klass.new(params)
|
66
|
+
end
|
67
|
+
|
68
|
+
def deserialize_enum(enum_module, enum_data)
|
69
|
+
variant_name = Utils.camelize(enum_data.fetch("kind"))
|
70
|
+
variant = enum_module.const_get(variant_name)
|
71
|
+
|
72
|
+
if variant.__kind__ == :unit
|
73
|
+
variant
|
74
|
+
elsif variant.__kind__ == :struct
|
75
|
+
deserialize_struct(variant, enum_data.fetch("data"))
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def deserialize_option(type, data)
|
80
|
+
if data.nil?
|
81
|
+
Object::Option::None
|
82
|
+
else
|
83
|
+
deserialized_data = deserialize(type, data)
|
84
|
+
option = Builder::Option.new(@type_module, type).build
|
85
|
+
option::Some.new(deserialized_data)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def deserialize_map(type, data)
|
90
|
+
map = Builder::Map.new(@type_module, type.key_type, type.value_type).build
|
91
|
+
|
92
|
+
deserialized_data = data.reduce({}) do |acc, (key, value)|
|
93
|
+
acc.merge(deserialize(type.key_type, key) => deserialize(type.value_type, value))
|
94
|
+
end
|
95
|
+
|
96
|
+
map.new(deserialized_data)
|
97
|
+
end
|
98
|
+
|
99
|
+
def deserialize_array(type, data)
|
100
|
+
array = Builder::Array.new(@type_module, type.type).build
|
101
|
+
array.new(*data.map { |x| deserialize(type.type, x) })
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LIT
|
4
|
+
# @api public
|
5
|
+
# @since 0.1.0
|
6
|
+
class Serializer
|
7
|
+
def initialize(object)
|
8
|
+
@object = object
|
9
|
+
end
|
10
|
+
|
11
|
+
def serialize
|
12
|
+
serialize_object(@object)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
# rubocop:disable Metrics/MethodLength
|
18
|
+
def serialize_object(object)
|
19
|
+
if object.is_a?(Object::Struct)
|
20
|
+
if object.is_a?(Object::EnumVariant)
|
21
|
+
serialize_enum_variant(object)
|
22
|
+
else
|
23
|
+
serialize_struct(object)
|
24
|
+
end
|
25
|
+
elsif object.is_a?(Module) && object <= Object::EnumVariant
|
26
|
+
serialize_enum_variant(object)
|
27
|
+
elsif object.is_a?(Object::Array)
|
28
|
+
serialize_array(object)
|
29
|
+
elsif object.is_a?(Object::Map)
|
30
|
+
serialize_map(object)
|
31
|
+
elsif object.is_a?(Object::Option::Some) || object == Object::Option::None
|
32
|
+
serialize_option(object)
|
33
|
+
elsif object.is_a?(String) ||
|
34
|
+
object.is_a?(Integer) ||
|
35
|
+
object.is_a?(Float) ||
|
36
|
+
object == true ||
|
37
|
+
object == false
|
38
|
+
object
|
39
|
+
else
|
40
|
+
raise InvalidObjectError, "invalid object: #{object}"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
# rubocop:enable Metrics/MethodLength
|
44
|
+
|
45
|
+
def serialize_struct(struct)
|
46
|
+
struct.instance_variables.reduce({}) do |hsh, ivar|
|
47
|
+
field_name = ivar.to_s.sub("@", "").to_sym
|
48
|
+
hsh.merge(field_name => serialize_object(struct.public_send(field_name)))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def serialize_enum_variant(variant)
|
53
|
+
data =
|
54
|
+
if variant.__kind__ == :struct
|
55
|
+
serialize_struct(variant)
|
56
|
+
else
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
|
60
|
+
{ kind: variant.__name__, data: data }
|
61
|
+
end
|
62
|
+
|
63
|
+
def serialize_array(array)
|
64
|
+
array.__values__.map { |value| serialize_object(value) }
|
65
|
+
end
|
66
|
+
|
67
|
+
def serialize_map(map)
|
68
|
+
map.__values__.reduce({}) do |acc, (key, value)|
|
69
|
+
acc.merge(serialize_object(key) => serialize_object(value))
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def serialize_option(option)
|
74
|
+
if option == Object::Option::None
|
75
|
+
nil
|
76
|
+
else
|
77
|
+
serialize_object(option.__value__)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|