taksi 0.1.0 → 0.2.2
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.
- checksums.yaml +4 -4
- data/README.md +115 -0
- data/lib/taksi/component.rb +86 -0
- data/lib/taksi/components/field.rb +92 -0
- data/lib/taksi/components/skeleton.rb +40 -0
- data/lib/taksi/interface.rb +80 -0
- data/lib/taksi/interfaces/skeleton.rb +36 -0
- data/lib/taksi/registry.rb +14 -15
- data/lib/taksi/values/dynamic.rb +11 -7
- data/lib/taksi/values/static.rb +8 -4
- data/lib/taksi/version.rb +1 -1
- data/lib/taksi.rb +6 -5
- metadata +24 -10
- data/lib/taksi/screen.rb +0 -68
- data/lib/taksi/screens/skeleton.rb +0 -36
- data/lib/taksi/widget.rb +0 -69
- data/lib/taksi/widgets/content_key.rb +0 -67
- data/lib/taksi/widgets/skeleton.rb +0 -30
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e57c4a5876a120b859b3642f790bf13d7f45a691ca74e2a3967d094589b2605a
|
|
4
|
+
data.tar.gz: da96e2c711489b11c4d24f88cb8e3ae738774c484772e3a9c5e0106ab13f5573
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1f52db226be934f1c70c127f2c75b105c1cdc65efd8443a76e338cb62769c95f2511e3b282521d753d6e34cc8791282889b7ef8d05bc471e3ad6b0fc87f8c316
|
|
7
|
+
data.tar.gz: 229d3fbd97e55eaa3e70a3dd735e2e3b6f4947d9b3b91a962d73a2680c8bccd0c9d04af2a63bff3fcd37810519e65af3c52adb37111c801b8098d2ebdec793e9
|
data/README.md
CHANGED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Taksi [](https://badge.fury.io/rb/taksi) [](https://github.com/taksi-br/taksi-ruby/actions/workflows/ci.yml) [](https://app.codacy.com/gh/taksi-br/taksi-ruby/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_coverage)
|
|
2
|
+
|
|
3
|
+
Application framework to build a backend driven UI in ruby.
|
|
4
|
+
|
|
5
|
+
## Considerations
|
|
6
|
+
|
|
7
|
+
This repository are in its **very early days** and **not ready for production** yet. If you want to help or understand what it is, get a look over our inspirations on the links below:
|
|
8
|
+
- https://medium.com/movile-tech/backend-driven-development-ios-d1c726f2913b
|
|
9
|
+
- https://engineering.q42.nl/server-driven-ui-at-primephonic/
|
|
10
|
+
- https://www.youtube.com/watch?v=vuCfKjOwZdU
|
|
11
|
+
|
|
12
|
+
Also, we're working in create a protocol documentation to explain the comunication details between frontend and backend.
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
In Taksi, every interface are composed by 1 to many components, those components are feed by data provided from the interface definition.
|
|
17
|
+
|
|
18
|
+
Defining a new component:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
class Components::Users::ProfileResume
|
|
22
|
+
include Taksi::Component.new('users/profile_resume')
|
|
23
|
+
|
|
24
|
+
content do
|
|
25
|
+
name Taksi::Dynamic
|
|
26
|
+
profile_kind Taksi::Static, 'resume'
|
|
27
|
+
|
|
28
|
+
details do
|
|
29
|
+
age Taksi::Dynamic
|
|
30
|
+
email Taksi::Dynamic
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Defining a new interface (in this example a interface interface):
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
class Interfaces::UserProfile
|
|
40
|
+
include Taksi::Interface.new('user_profile')
|
|
41
|
+
|
|
42
|
+
add Components::Users::ProfileResume, with: :profile_data
|
|
43
|
+
|
|
44
|
+
attr_accessor :user
|
|
45
|
+
|
|
46
|
+
def profile_data
|
|
47
|
+
{
|
|
48
|
+
name: user.name,
|
|
49
|
+
details: {
|
|
50
|
+
age: user.age,
|
|
51
|
+
email: user.email,
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
From those definitions you can set up the skeleton or strip the data:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
user_profile = Interfaces::UserProfile.new
|
|
62
|
+
user_profile.skeleton.as_json
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Which provide us:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"components": [
|
|
70
|
+
{
|
|
71
|
+
"name": "users/profile_resume",
|
|
72
|
+
"identifier": "component$0",
|
|
73
|
+
"requires_data": true,
|
|
74
|
+
"content": {
|
|
75
|
+
"name": null,
|
|
76
|
+
"profile_kind": "resume",
|
|
77
|
+
"details": {
|
|
78
|
+
"age": null,
|
|
79
|
+
"email": null
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Then, you can strip the data off:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
user_profile.user = User.find(logged_user_id)
|
|
91
|
+
user_profile.data.as_json
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"interface_data": [
|
|
97
|
+
{
|
|
98
|
+
"identifier": "component$0",
|
|
99
|
+
"content": {
|
|
100
|
+
"name": "Israel Trindade",
|
|
101
|
+
"details": {
|
|
102
|
+
"age": 29,
|
|
103
|
+
"email": "irto@outlook.com",
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Supported Ruby versions
|
|
112
|
+
|
|
113
|
+
This library officially supports the following Ruby versions:
|
|
114
|
+
|
|
115
|
+
* MRI `>= 2.7`
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Taksi
|
|
4
|
+
# A custom module to turn a class into a component on taksi protocol
|
|
5
|
+
#
|
|
6
|
+
# ```ruby
|
|
7
|
+
# class CustomComponent
|
|
8
|
+
# include Taksi::Component.new('customs/component_name')
|
|
9
|
+
#
|
|
10
|
+
# content do
|
|
11
|
+
# field_name Taksi::Static
|
|
12
|
+
# end
|
|
13
|
+
# end
|
|
14
|
+
# ```
|
|
15
|
+
#
|
|
16
|
+
class Component < ::Module
|
|
17
|
+
attr_reader :identifier
|
|
18
|
+
|
|
19
|
+
def initialize(identifier)
|
|
20
|
+
@identifier = identifier
|
|
21
|
+
super()
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def included(klass)
|
|
25
|
+
klass.extend(ClassMethods)
|
|
26
|
+
klass.include(InstanceMethods)
|
|
27
|
+
|
|
28
|
+
klass.definition = self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
module ClassMethods
|
|
32
|
+
attr_reader :content_builder
|
|
33
|
+
|
|
34
|
+
def definition=(component_definition)
|
|
35
|
+
@component_definition = component_definition
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def identifier
|
|
39
|
+
@component_definition.identifier
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def content(&block)
|
|
43
|
+
@content_builder = block
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
module InstanceMethods
|
|
49
|
+
attr_reader :interface_definition, :datasource, :skeleton
|
|
50
|
+
|
|
51
|
+
def initialize(interface_definition, with: nil)
|
|
52
|
+
@interface_definition = interface_definition
|
|
53
|
+
@datasource = with
|
|
54
|
+
@skeleton = @interface_definition.skeleton.create_component(self.class.identifier,
|
|
55
|
+
&self.class.content_builder)
|
|
56
|
+
super()
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def id
|
|
60
|
+
@skeleton.id
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def content_for(interface)
|
|
64
|
+
data = interface.send(datasource)
|
|
65
|
+
|
|
66
|
+
skeleton.fields.each_with_object({}) do |field, obj|
|
|
67
|
+
load_data_from_key_to_object(data, field, obj)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def load_data_from_key_to_object(data, field, obj)
|
|
74
|
+
splitted_full_path = field.key.to_s.split('.')
|
|
75
|
+
setter_key = splitted_full_path.pop
|
|
76
|
+
splitted_full_path.shift # remove content root key, as it makes no sense in data object
|
|
77
|
+
|
|
78
|
+
relative_object = splitted_full_path.reduce(obj) do |memo, path_part|
|
|
79
|
+
memo[path_part.to_sym] ||= {}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
relative_object[setter_key.to_sym] = field.fetch_from(data)
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Taksi
|
|
4
|
+
module Components
|
|
5
|
+
class Field
|
|
6
|
+
attr_reader :skeleton, :name, :value, :parent
|
|
7
|
+
|
|
8
|
+
def initialize(skeleton, name, *args, parent: nil, &block)
|
|
9
|
+
@skeleton = skeleton
|
|
10
|
+
@name = name.to_sym
|
|
11
|
+
@parent = parent
|
|
12
|
+
|
|
13
|
+
raise <<~MESSAGE unless args.size.positive? || block_given?
|
|
14
|
+
You must provide a value or a block definition to build field
|
|
15
|
+
MESSAGE
|
|
16
|
+
|
|
17
|
+
@value = args.shift.new(skeleton, name, *args) if args.size.positive?
|
|
18
|
+
@nested_fields = []
|
|
19
|
+
|
|
20
|
+
instance_exec(&block) if block_given?
|
|
21
|
+
@defined = true
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def key
|
|
25
|
+
return name if parent.nil? || parent.root?
|
|
26
|
+
|
|
27
|
+
"#{parent.name}.#{name}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Fetches the data for in `data` for the current field
|
|
31
|
+
# @return any
|
|
32
|
+
def fetch_from(data)
|
|
33
|
+
return value.as_json if value.static?
|
|
34
|
+
|
|
35
|
+
return data[name] if parent.nil? || parent.root?
|
|
36
|
+
|
|
37
|
+
parent.fetch_from(data)[name]
|
|
38
|
+
rescue NoMethodError
|
|
39
|
+
raise NameError, "Couldn't fetch #{key.inspect} from data: #{data.inspect}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Turns the field into his json representation
|
|
43
|
+
# The returned hash is compatible with the skeleton json specification
|
|
44
|
+
# @return Hash
|
|
45
|
+
def as_json
|
|
46
|
+
return {name => @nested_fields.map(&:as_json).inject({}, &:merge)} if nested?
|
|
47
|
+
|
|
48
|
+
{name => value.as_json}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Builds up a interator over all fields included nested ones
|
|
52
|
+
# @returns Enumerable
|
|
53
|
+
def fields
|
|
54
|
+
Enumerator.new do |yielder|
|
|
55
|
+
@nested_fields.each do |field|
|
|
56
|
+
if field.nested?
|
|
57
|
+
field.fields.each(&yielder)
|
|
58
|
+
else
|
|
59
|
+
yielder << field
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def nested?
|
|
66
|
+
@value.nil?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def root?
|
|
70
|
+
@parent.nil?
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def dynamic?
|
|
74
|
+
return @nested_fields.any?(&:dynamic?) if @value.nil?
|
|
75
|
+
|
|
76
|
+
@value.dynamic?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def method_missing(name, *args, &block)
|
|
80
|
+
return super if @defined
|
|
81
|
+
|
|
82
|
+
@nested_fields << self.class.new(skeleton, name, *args, parent: self, &block)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def respond_to_missing?(name, *)
|
|
86
|
+
return super if @defined
|
|
87
|
+
|
|
88
|
+
true
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Taksi
|
|
4
|
+
module Components
|
|
5
|
+
class Skeleton
|
|
6
|
+
attr_reader :parent, :name, :content
|
|
7
|
+
|
|
8
|
+
def initialize(parent, name, &block)
|
|
9
|
+
@parent = parent
|
|
10
|
+
@name = name
|
|
11
|
+
|
|
12
|
+
raise 'To build a component you need to provide a `content` block' unless block_given?
|
|
13
|
+
|
|
14
|
+
@content = ::Taksi::Components::Field.new(self, :content, &block)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def id
|
|
18
|
+
parent.id_of(self)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def fields
|
|
22
|
+
content.fields
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def dynamic?
|
|
26
|
+
@content.dynamic?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def as_json
|
|
30
|
+
{
|
|
31
|
+
name: name,
|
|
32
|
+
identifier: id,
|
|
33
|
+
requires_data: dynamic?
|
|
34
|
+
}.tap do |json|
|
|
35
|
+
json.merge!(content.as_json)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Taksi
|
|
4
|
+
# A custom module that turns a class into a interface on taksi protocol
|
|
5
|
+
#
|
|
6
|
+
class Interface < ::Module
|
|
7
|
+
attr_reader :interface_name, :version_pattern, :alternatives
|
|
8
|
+
|
|
9
|
+
# Finds for a interface by its name and the current version
|
|
10
|
+
# @param name [String]
|
|
11
|
+
# @param version [String] just like '0.2.1'
|
|
12
|
+
# @param alternatives: [Array]
|
|
13
|
+
# @raises`::Taksi::Registry::InterfaceNotFoundError`
|
|
14
|
+
# @return Class the class of interface
|
|
15
|
+
def self.find(name, version, alternative: nil)
|
|
16
|
+
::Taksi::Registry.find(name, version, alternative)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(name, version_pattern = nil, alternatives: nil)
|
|
20
|
+
@interface_name = name
|
|
21
|
+
@version_pattern = ::Gem::Requirement.new(version_pattern)
|
|
22
|
+
@alternatives = alternatives
|
|
23
|
+
super()
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def included(klass)
|
|
27
|
+
klass.extend(ClassMethods)
|
|
28
|
+
klass.include(InstanceMethods)
|
|
29
|
+
|
|
30
|
+
klass.initiate(self)
|
|
31
|
+
|
|
32
|
+
::Taksi::Registry.add(klass, interface_name)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
module ClassMethods
|
|
36
|
+
attr_reader :skeleton
|
|
37
|
+
|
|
38
|
+
def find(version, alternative = nil)
|
|
39
|
+
::Taksi::Registry.find(@interface_definition.interface_name, version, alternative)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def initiate(interface_definition)
|
|
43
|
+
@components = []
|
|
44
|
+
@interface_definition = interface_definition
|
|
45
|
+
@skeleton = ::Taksi::Interfaces::Skeleton.new
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def add(component_class, with: nil)
|
|
49
|
+
@components << component_class.new(self, with: with)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def components
|
|
53
|
+
@components.each
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def version_pattern
|
|
57
|
+
@interface_definition.version_pattern
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def alternatives
|
|
61
|
+
@interface_definition.alternatives
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
module InstanceMethods
|
|
66
|
+
def skeleton
|
|
67
|
+
self.class.skeleton
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def data
|
|
71
|
+
self.class.components.map do |component|
|
|
72
|
+
{
|
|
73
|
+
identifier: component.id,
|
|
74
|
+
content: component.content_for(self)
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Taksi
|
|
4
|
+
module Interfaces
|
|
5
|
+
class Skeleton
|
|
6
|
+
class ComponentNotFoundError < StandardError; end
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@component_skeletons = []
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def create_component(identifier, &block)
|
|
13
|
+
::Taksi::Components::Skeleton.new(self, identifier, &block).tap do |component|
|
|
14
|
+
add(component)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def add(component_skeleton)
|
|
19
|
+
@component_skeletons << component_skeleton
|
|
20
|
+
self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def id_of(component)
|
|
24
|
+
index = @component_skeletons.index(component)
|
|
25
|
+
|
|
26
|
+
raise ComponentNotFoundError unless index
|
|
27
|
+
|
|
28
|
+
"component$#{index}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def as_json(*)
|
|
32
|
+
{components: @component_skeletons.map(&:as_json)}
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/taksi/registry.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'forwardable'
|
|
4
3
|
module Taksi
|
|
5
4
|
class Registry
|
|
6
5
|
include ::Singleton
|
|
@@ -11,8 +10,8 @@ module Taksi
|
|
|
11
10
|
def_delegators :instance, :find, :add, :clear!
|
|
12
11
|
end
|
|
13
12
|
|
|
14
|
-
class
|
|
15
|
-
class
|
|
13
|
+
class InterfaceNotFoundError < ::StandardError; end
|
|
14
|
+
class InterfaceAltenativeNotFoundError < ::StandardError; end
|
|
16
15
|
|
|
17
16
|
def initialize
|
|
18
17
|
clear!
|
|
@@ -21,36 +20,36 @@ module Taksi
|
|
|
21
20
|
def add(klass, name)
|
|
22
21
|
sym_name = name.to_sym
|
|
23
22
|
|
|
24
|
-
@
|
|
25
|
-
@
|
|
23
|
+
@interfaces[sym_name] ||= []
|
|
24
|
+
@interfaces[sym_name] << klass
|
|
26
25
|
end
|
|
27
26
|
|
|
28
27
|
def clear!
|
|
29
|
-
@
|
|
28
|
+
@interfaces = {}
|
|
30
29
|
end
|
|
31
30
|
|
|
32
31
|
def find(name, version, alternative = nil)
|
|
33
|
-
|
|
32
|
+
interfaces_from_name = @interfaces[name.to_sym]
|
|
34
33
|
|
|
35
|
-
raise
|
|
34
|
+
raise InterfaceNotFoundError if interfaces_from_name.nil?
|
|
36
35
|
|
|
37
36
|
parsed_version = ::Gem::Version.new(version)
|
|
38
37
|
|
|
39
|
-
|
|
40
|
-
next false unless
|
|
38
|
+
found_interface = interfaces_from_name.find do |interface|
|
|
39
|
+
next false unless interface.version_pattern.satisfied_by?(parsed_version)
|
|
41
40
|
|
|
42
|
-
next true if alternative.
|
|
41
|
+
next true if alternative.nil?
|
|
43
42
|
|
|
44
|
-
next true if
|
|
43
|
+
next true if interface.alternatives.nil?
|
|
45
44
|
|
|
46
|
-
next true if
|
|
45
|
+
next true if interface.alternatives.include?(alternative)
|
|
47
46
|
|
|
48
47
|
false
|
|
49
48
|
end
|
|
50
49
|
|
|
51
|
-
raise
|
|
50
|
+
raise InterfaceNotFoundError if found_interface.nil?
|
|
52
51
|
|
|
53
|
-
|
|
52
|
+
found_interface
|
|
54
53
|
end
|
|
55
54
|
end
|
|
56
55
|
end
|
data/lib/taksi/values/dynamic.rb
CHANGED
|
@@ -3,27 +3,31 @@
|
|
|
3
3
|
module Taksi
|
|
4
4
|
module Values
|
|
5
5
|
class Dynamic
|
|
6
|
-
attr_reader :
|
|
6
|
+
attr_reader :component, :name, :field
|
|
7
7
|
|
|
8
|
-
def initialize(
|
|
9
|
-
@
|
|
8
|
+
def initialize(component, name, field = nil)
|
|
9
|
+
@component = component
|
|
10
10
|
@name = name
|
|
11
|
-
@
|
|
11
|
+
@field = field
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def path
|
|
15
|
-
return
|
|
15
|
+
return field if field
|
|
16
16
|
|
|
17
|
-
"#{
|
|
17
|
+
"#{component.id}.#{name}"
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def as_json
|
|
21
|
-
|
|
21
|
+
nil
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def dynamic?
|
|
25
25
|
true
|
|
26
26
|
end
|
|
27
|
+
|
|
28
|
+
def static?
|
|
29
|
+
false
|
|
30
|
+
end
|
|
27
31
|
end
|
|
28
32
|
end
|
|
29
33
|
|
data/lib/taksi/values/static.rb
CHANGED
|
@@ -3,21 +3,25 @@
|
|
|
3
3
|
module Taksi
|
|
4
4
|
module Values
|
|
5
5
|
class Static
|
|
6
|
-
attr_reader :
|
|
6
|
+
attr_reader :component, :name, :value
|
|
7
7
|
|
|
8
|
-
def initialize(
|
|
9
|
-
@
|
|
8
|
+
def initialize(component, name, value)
|
|
9
|
+
@component = component
|
|
10
10
|
@name = name
|
|
11
11
|
@value = value
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def as_json
|
|
15
|
-
|
|
15
|
+
value
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def dynamic?
|
|
19
19
|
false
|
|
20
20
|
end
|
|
21
|
+
|
|
22
|
+
def static?
|
|
23
|
+
true
|
|
24
|
+
end
|
|
21
25
|
end
|
|
22
26
|
end
|
|
23
27
|
|
data/lib/taksi/version.rb
CHANGED
data/lib/taksi.rb
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'singleton'
|
|
4
|
+
require 'forwardable'
|
|
4
5
|
|
|
5
6
|
require 'taksi/version'
|
|
6
7
|
|
|
7
8
|
require 'taksi/values/dynamic'
|
|
8
9
|
require 'taksi/values/static'
|
|
9
10
|
|
|
10
|
-
require 'taksi/
|
|
11
|
-
require 'taksi/
|
|
12
|
-
require 'taksi/
|
|
11
|
+
require 'taksi/component'
|
|
12
|
+
require 'taksi/components/skeleton'
|
|
13
|
+
require 'taksi/components/field'
|
|
13
14
|
|
|
14
15
|
require 'taksi/registry'
|
|
15
16
|
|
|
16
|
-
require 'taksi/
|
|
17
|
-
require 'taksi/
|
|
17
|
+
require 'taksi/interface'
|
|
18
|
+
require 'taksi/interfaces/skeleton'
|
|
18
19
|
|
|
19
20
|
module Taksi
|
|
20
21
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: taksi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Israel Trindade
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2023-07-
|
|
11
|
+
date: 2023-07-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -24,6 +24,20 @@ dependencies:
|
|
|
24
24
|
- - ">="
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
26
|
version: '0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rake
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
27
41
|
- !ruby/object:Gem::Dependency
|
|
28
42
|
name: rspec
|
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -38,8 +52,8 @@ dependencies:
|
|
|
38
52
|
- - ">="
|
|
39
53
|
- !ruby/object:Gem::Version
|
|
40
54
|
version: '0'
|
|
41
|
-
description: Useful
|
|
42
|
-
|
|
55
|
+
description: Useful toolset to build a backend over the concept of backend-driven
|
|
56
|
+
UI (aka. backend for frontend).
|
|
43
57
|
email:
|
|
44
58
|
- irto@outlook.com
|
|
45
59
|
executables: []
|
|
@@ -50,15 +64,15 @@ files:
|
|
|
50
64
|
- README.md
|
|
51
65
|
- Rakefile
|
|
52
66
|
- lib/taksi.rb
|
|
67
|
+
- lib/taksi/component.rb
|
|
68
|
+
- lib/taksi/components/field.rb
|
|
69
|
+
- lib/taksi/components/skeleton.rb
|
|
70
|
+
- lib/taksi/interface.rb
|
|
71
|
+
- lib/taksi/interfaces/skeleton.rb
|
|
53
72
|
- lib/taksi/registry.rb
|
|
54
|
-
- lib/taksi/screen.rb
|
|
55
|
-
- lib/taksi/screens/skeleton.rb
|
|
56
73
|
- lib/taksi/values/dynamic.rb
|
|
57
74
|
- lib/taksi/values/static.rb
|
|
58
75
|
- lib/taksi/version.rb
|
|
59
|
-
- lib/taksi/widget.rb
|
|
60
|
-
- lib/taksi/widgets/content_key.rb
|
|
61
|
-
- lib/taksi/widgets/skeleton.rb
|
|
62
76
|
homepage:
|
|
63
77
|
licenses: []
|
|
64
78
|
metadata:
|
|
@@ -84,5 +98,5 @@ requirements: []
|
|
|
84
98
|
rubygems_version: 3.3.26
|
|
85
99
|
signing_key:
|
|
86
100
|
specification_version: 4
|
|
87
|
-
summary:
|
|
101
|
+
summary: Application framework to build a backend driven UI in ruby
|
|
88
102
|
test_files: []
|
data/lib/taksi/screen.rb
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Taksi
|
|
4
|
-
class Screen < ::Module
|
|
5
|
-
attr_reader :page_name, :version_pattern, :alternatives
|
|
6
|
-
|
|
7
|
-
def self.find(name, version, alternative: nil)
|
|
8
|
-
::Taksi::Registry.find(name, version, alternative)
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def initialize(name, version_pattern = nil, alternatives: nil)
|
|
12
|
-
@page_name = name
|
|
13
|
-
@version_pattern = ::Gem::Requirement.new(version_pattern)
|
|
14
|
-
@alternatives = alternatives
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def included(klass)
|
|
18
|
-
klass.extend(ClassMethods)
|
|
19
|
-
klass.include(InstanceMethods)
|
|
20
|
-
|
|
21
|
-
klass.initiate(self)
|
|
22
|
-
|
|
23
|
-
::Taksi::Registry.add(klass, page_name)
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
module ClassMethods
|
|
27
|
-
attr_reader :widgets, :skeleton
|
|
28
|
-
|
|
29
|
-
def find(version, alternative = nil)
|
|
30
|
-
::Taksi::Registry.find(page_name, version, alternative)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def initiate(screen_definition)
|
|
34
|
-
@widgets = []
|
|
35
|
-
@screen_definition = screen_definition
|
|
36
|
-
@skeleton = ::Taksi::Screens::Skeleton.new
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def add(widget_class, with: nil)
|
|
40
|
-
@widgets << widget_class.new(self, with: with)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def widgets
|
|
44
|
-
@widgets.each
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def version_pattern
|
|
48
|
-
@screen_definition.version_pattern
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def alternatives
|
|
52
|
-
@screen_definition.alternatives
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
module InstanceMethods
|
|
57
|
-
def skeleton
|
|
58
|
-
self.class.skeleton
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def data
|
|
62
|
-
self.class.widgets.each_with_object({}) do |widget, obj|
|
|
63
|
-
obj.merge!(widget.data_for(self).as_json)
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
end
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Taksi
|
|
4
|
-
module Screens
|
|
5
|
-
class Skeleton
|
|
6
|
-
class WidgetNotFoundError < StandardError; end
|
|
7
|
-
|
|
8
|
-
def initialize
|
|
9
|
-
@widget_skeletons = []
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def create_widget(identifier, &block)
|
|
13
|
-
::Taksi::Widgets::Skeleton.new(self, identifier, &block).tap do |widget|
|
|
14
|
-
add(widget)
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def add(widget_skeleton)
|
|
19
|
-
@widget_skeletons << widget_skeleton
|
|
20
|
-
self
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def id_of(widget)
|
|
24
|
-
index = @widget_skeletons.index(widget)
|
|
25
|
-
|
|
26
|
-
raise WidgetNotFoundError unless index
|
|
27
|
-
|
|
28
|
-
"widget$#{index}"
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def as_json(*)
|
|
32
|
-
{widgets: @widget_skeletons.map(&:as_json)}
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
data/lib/taksi/widget.rb
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Taksi
|
|
4
|
-
class Widget < ::Module
|
|
5
|
-
attr_reader :identifier
|
|
6
|
-
|
|
7
|
-
def initialize(identifier)
|
|
8
|
-
@identifier = identifier
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def included(klass)
|
|
12
|
-
klass.extend(ClassMethods)
|
|
13
|
-
klass.include(InstanceMethods)
|
|
14
|
-
|
|
15
|
-
klass.definition = self
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
module ClassMethods
|
|
19
|
-
attr_reader :content_builder
|
|
20
|
-
|
|
21
|
-
def definition=(widget_definition)
|
|
22
|
-
@widget_definition = widget_definition
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def identifier
|
|
26
|
-
@widget_definition.identifier
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def content(&block)
|
|
30
|
-
@content_builder = block
|
|
31
|
-
self
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
module InstanceMethods
|
|
36
|
-
attr_reader :page_definition, :datasource, :skeleton
|
|
37
|
-
|
|
38
|
-
def initialize(page_definition, with: nil)
|
|
39
|
-
@page_definition = page_definition
|
|
40
|
-
@datasource = with
|
|
41
|
-
@skeleton = @page_definition.skeleton.create_widget(self.class.identifier,
|
|
42
|
-
&self.class.content_builder)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def data_for(page_instance)
|
|
46
|
-
data = page_instance.send(datasource)
|
|
47
|
-
|
|
48
|
-
skeleton.keys.each_with_object({}) do |content_key, obj|
|
|
49
|
-
next unless content_key.value.dynamic?
|
|
50
|
-
|
|
51
|
-
load_data_from_key_to_object(data, content_key, obj)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
private
|
|
56
|
-
|
|
57
|
-
def load_data_from_key_to_object(data, content_key, obj)
|
|
58
|
-
splitted_full_path = content_key.value.path.split('.')
|
|
59
|
-
setter_key = splitted_full_path.pop
|
|
60
|
-
|
|
61
|
-
relative_object = splitted_full_path.reduce(obj) do |memo, path_part|
|
|
62
|
-
memo[path_part] ||= {}
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
relative_object[setter_key] = content_key.fetch_from(data)
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Taksi
|
|
4
|
-
module Widgets
|
|
5
|
-
class ContentKey
|
|
6
|
-
attr_reader :skeleton, :key, :value, :parent
|
|
7
|
-
|
|
8
|
-
def initialize(skeleton, key, *args, parent: nil, &block)
|
|
9
|
-
@skeleton = skeleton
|
|
10
|
-
@key = key.to_sym
|
|
11
|
-
@parent = parent
|
|
12
|
-
|
|
13
|
-
@value = args.shift.new(skeleton, full_key, *args) if args.size.positive?
|
|
14
|
-
@nested_keys = []
|
|
15
|
-
|
|
16
|
-
instance_exec(&block) if block_given?
|
|
17
|
-
@defined = true
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def full_key
|
|
21
|
-
return key if parent.nil? || parent.root?
|
|
22
|
-
|
|
23
|
-
"#{parent.full_key}.#{key}"
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def fetch_from(data)
|
|
27
|
-
return data[key] if parent.nil? || parent.root?
|
|
28
|
-
|
|
29
|
-
parent.fetch_from(data)[key]
|
|
30
|
-
rescue NoMethodError
|
|
31
|
-
raise NameError, "Couldn't fetch #{key.inspect} from data: #{data.inspect}"
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def as_json
|
|
35
|
-
return {key => @nested_keys.map(&:as_json).inject({}, &:merge)} if nested?
|
|
36
|
-
|
|
37
|
-
{key => value.as_json}
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def keys
|
|
41
|
-
Enumerator.new do |yielder|
|
|
42
|
-
@nested_keys.each do |key|
|
|
43
|
-
if key.nested?
|
|
44
|
-
key.keys.each(&yielder)
|
|
45
|
-
else
|
|
46
|
-
yielder << key
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def nested?
|
|
53
|
-
@value.nil?
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def root?
|
|
57
|
-
@parent.nil?
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def method_missing(name, *args, &block)
|
|
61
|
-
return super if @defined
|
|
62
|
-
|
|
63
|
-
@nested_keys << self.class.new(skeleton, name, *args, parent: self, &block)
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Taksi
|
|
4
|
-
module Widgets
|
|
5
|
-
class Skeleton
|
|
6
|
-
attr_reader :parent, :identifier, :content
|
|
7
|
-
|
|
8
|
-
def initialize(parent, identifier, &block)
|
|
9
|
-
@parent = parent
|
|
10
|
-
@identifier = identifier
|
|
11
|
-
|
|
12
|
-
@content = ::Taksi::Widgets::ContentKey.new(self, :content, &block)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def id
|
|
16
|
-
parent.id_of(self)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def keys
|
|
20
|
-
content.keys
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def as_json
|
|
24
|
-
{identifier: identifier}.tap do |json|
|
|
25
|
-
json.merge!(content.as_json)
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|