pakyow-ui 0.11.3 → 1.0.0.rc1

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 (44) hide show
  1. checksums.yaml +5 -5
  2. data/{pakyow-ui/CHANGELOG.md → CHANGELOG.md} +0 -0
  3. data/LICENSE +4 -0
  4. data/{pakyow-ui/README.md → README.md} +1 -2
  5. data/lib/pakyow/ui/behavior/recording.rb +51 -0
  6. data/lib/pakyow/ui/behavior/rendering/install_transforms.rb +47 -0
  7. data/lib/pakyow/ui/behavior/rendering.rb +105 -0
  8. data/lib/pakyow/ui/behavior/timeouts.rb +31 -0
  9. data/lib/pakyow/ui/framework.rb +75 -0
  10. data/lib/pakyow/ui/handler.rb +42 -0
  11. data/lib/pakyow/ui/helpers.rb +19 -0
  12. data/lib/pakyow/ui/recordable/attribute.rb +39 -0
  13. data/lib/pakyow/ui/recordable/attributes.rb +50 -0
  14. data/lib/pakyow/ui/recordable/helpers/client_remapping.rb +30 -0
  15. data/lib/pakyow/ui/recordable.rb +303 -0
  16. data/lib/pakyow/ui.rb +9 -0
  17. metadata +46 -60
  18. data/pakyow-ui/LICENSE +0 -20
  19. data/pakyow-ui/lib/pakyow/ui/base.rb +0 -26
  20. data/pakyow-ui/lib/pakyow/ui/channel_builder.rb +0 -55
  21. data/pakyow-ui/lib/pakyow/ui/config.rb +0 -11
  22. data/pakyow-ui/lib/pakyow/ui/ext/app.rb +0 -52
  23. data/pakyow-ui/lib/pakyow/ui/ext/view_context.rb +0 -30
  24. data/pakyow-ui/lib/pakyow/ui/fetch_view_handler.rb +0 -68
  25. data/pakyow-ui/lib/pakyow/ui/helpers.rb +0 -15
  26. data/pakyow-ui/lib/pakyow/ui/mock_mutation_eval.rb +0 -25
  27. data/pakyow-ui/lib/pakyow/ui/mutable.rb +0 -99
  28. data/pakyow-ui/lib/pakyow/ui/mutable_data.rb +0 -21
  29. data/pakyow-ui/lib/pakyow/ui/mutate_context.rb +0 -64
  30. data/pakyow-ui/lib/pakyow/ui/mutation_set.rb +0 -38
  31. data/pakyow-ui/lib/pakyow/ui/mutation_store.rb +0 -41
  32. data/pakyow-ui/lib/pakyow/ui/mutator.rb +0 -63
  33. data/pakyow-ui/lib/pakyow/ui/no_op_view.rb +0 -87
  34. data/pakyow-ui/lib/pakyow/ui/registries/redis_mutation_registry.rb +0 -70
  35. data/pakyow-ui/lib/pakyow/ui/registries/simple_mutation_registry.rb +0 -37
  36. data/pakyow-ui/lib/pakyow/ui/ui.rb +0 -81
  37. data/pakyow-ui/lib/pakyow/ui/ui_attrs.rb +0 -40
  38. data/pakyow-ui/lib/pakyow/ui/ui_component.rb +0 -68
  39. data/pakyow-ui/lib/pakyow/ui/ui_context.rb +0 -16
  40. data/pakyow-ui/lib/pakyow/ui/ui_instructable.rb +0 -117
  41. data/pakyow-ui/lib/pakyow/ui/ui_request.rb +0 -14
  42. data/pakyow-ui/lib/pakyow/ui/ui_view.rb +0 -200
  43. data/pakyow-ui/lib/pakyow/ui.rb +0 -1
  44. data/pakyow-ui/lib/pakyow-ui.rb +0 -1
@@ -1,30 +0,0 @@
1
- module Pakyow
2
- module Presenter
3
- class ViewContext
4
- MSG_NONCOMPONENT = 'Cannot subscribe a non-component view'
5
-
6
- # Mutates a view with a registered mutator.
7
- #
8
- # @api public
9
- def mutate(mutator, data: nil, with: nil)
10
- Pakyow::UI::Mutator.instance.mutate(mutator, self, data || with || [])
11
- end
12
-
13
- # Subscribes a view and sets the `data-channel` attribute.
14
- #
15
- # @api public
16
- def subscribe(qualifications = {})
17
- fail ArgumentError, MSG_NONCOMPONENT unless component?
18
-
19
- channel = Pakyow::UI::ChannelBuilder.build(
20
- component: component_name,
21
- qualifications: qualifications
22
- )
23
-
24
- context.socket.subscribe(channel)
25
- attrs.send(:'data-channel=', channel)
26
- self
27
- end
28
- end
29
- end
30
- end
@@ -1,68 +0,0 @@
1
- require_relative 'no_op_view'
2
-
3
- # Makes it possible to fetch a particular part of a view for a path. Calls a
4
- # route with all view actions becoming no-ops. Then a query is run against the
5
- # final view, pulling out the part that was requested.
6
- #
7
- # Expects the following in the message:
8
- #
9
- # - uri: the route to call
10
- # - lookup: the view query
11
- #
12
- # Lookup currently supports the following keys:
13
- #
14
- # - channel
15
- # - version
16
- # - container
17
- # - partial
18
- # - scope
19
- # - prop
20
- #
21
- Pakyow::Realtime.handler :'fetch-view' do |message, session, response|
22
- env = Rack::MockRequest.env_for(message['uri'])
23
- env['pakyow.socket'] = true
24
- env['rack.session'] = session
25
-
26
- context = Pakyow::CallContext.new(env)
27
-
28
- def context.view
29
- Pakyow::Presenter::NoOpView.new(
30
- Pakyow::Presenter::ViewContext.new(@presenter.view, self),
31
- self
32
- )
33
- end
34
-
35
- app_response = context.process.finish
36
-
37
- body = ''
38
- lookup = message['lookup']
39
- view = context.presenter.view
40
-
41
- channel = lookup['channel']
42
-
43
- if channel
44
- unqualified_channel = channel.split('::')[0]
45
- view_for_channel = view.composed.doc.channel(unqualified_channel)
46
-
47
- if view_for_channel
48
- view_for_channel.set_attribute(:'data-channel', channel)
49
- body = view_for_channel.to_html
50
- end
51
- else
52
- lookup.each_pair do |key, value|
53
- next if key == 'version'
54
- view = view.send(key.to_sym, value.to_sym)
55
- end
56
-
57
- if view.is_a?(Pakyow::Presenter::ViewVersion)
58
- body = view.use((lookup['version'] || :default).to_sym).to_html
59
- else
60
- body = view.to_html
61
- end
62
- end
63
-
64
- response[:status] = app_response[0]
65
- response[:headers] = app_response[1]
66
- response[:body] = body
67
- response
68
- end
@@ -1,15 +0,0 @@
1
- module Pakyow
2
- module Helpers
3
- def ui
4
- return @ui unless @ui.nil?
5
-
6
- ui_dup = Pakyow.app.ui.dup
7
- ui_dup.context = self
8
- @ui = ui_dup
9
- end
10
-
11
- def data(scope)
12
- ui.mutator.mutable(scope, self)
13
- end
14
- end
15
- end
@@ -1,25 +0,0 @@
1
- module Pakyow
2
- module Presenter
3
- # Used by NoOpView to perform mutations in a no-op manner.
4
- #
5
- # @api private
6
- class MockMutationEval
7
- def initialize(mutation_name, relation_name, view)
8
- @mutation_name = mutation_name
9
- @relation_name = relation_name
10
- @view = view
11
- end
12
-
13
- # NOTE we don't care about qualifiers here since we're just getting
14
- # the proper view template; not actually setting it up with data
15
- def subscribe(*_args)
16
- channel = Pakyow::UI::ChannelBuilder.build(
17
- scope: @view.scoped_as,
18
- mutation: @mutation_name
19
- )
20
-
21
- @view.attrs.send(:'data-channel=', channel)
22
- end
23
- end
24
- end
25
- end
@@ -1,99 +0,0 @@
1
- require_relative 'mutable_data'
2
-
3
- # TODO: make it possible to register this as data instead of mutables
4
-
5
- module Pakyow
6
- module UI
7
- # Mutables enable PakyowUI to automatically handle changes in application
8
- # state by interacting with the data layer in a declarative manner.
9
- #
10
- # Wraps a data source (such as a model object) and provides a convenient
11
- # interface for defining and executing queries and actions. Queries accept
12
- # parameters and return data sets. Actions cause a state change in
13
- # application state.
14
- #
15
- # Once defined, all interactions with the data layer should occur through
16
- # Mutables via the `data` helper method. When an action is performed that
17
- # changes the state of the application, Pakyow will propogate the change
18
- # through to all other connected clients automatically.
19
- #
20
- # Mutables should be registered with the `Pakyow::App.mutable` helper. The
21
- # defined block will be executed in context of a `Mutable` instance.
22
- #
23
- # @api public
24
- class Mutable
25
- include Helpers
26
-
27
- attr_reader :context
28
-
29
- # @api private
30
- def initialize(context, scope, &block)
31
- @context = context
32
- @scope = scope
33
- @actions = {}
34
- @queries = {}
35
-
36
- instance_exec(&block)
37
- end
38
-
39
- # Sets the model object.
40
- #
41
- # @api public
42
- def model(model_class, type: nil)
43
- @model_class = model_class
44
-
45
- return if type.nil?
46
- @model_type = type
47
-
48
- # TODO: load default actions / queries based on type
49
- end
50
-
51
- # Defines an action.
52
- #
53
- # @api public
54
- def action(name, mutation: true, &block)
55
- @actions[name] = {
56
- block: block,
57
- mutation: mutation
58
- }
59
- end
60
-
61
- # Defines a query.
62
- #
63
- # @api public
64
- def query(name, &block)
65
- @queries[name] = block
66
- end
67
-
68
- # Handles calling queries or actions. Enables convenience like:
69
- #
70
- # data(:some_data).{action or query}
71
- #
72
- # @api public
73
- def method_missing(method, *args)
74
- action = @actions[method]
75
- query = @queries[method]
76
-
77
- if action
78
- call_action(action, *args)
79
- elsif query
80
- call_query(query, method, *args)
81
- else
82
- fail ArgumentError, "Could not find query or action named #{method}"
83
- end
84
- end
85
-
86
- private
87
-
88
- def call_action(action, *args)
89
- result = action[:block].call(*args)
90
- @context.ui.mutated(@scope, result, @context) if action[:mutation]
91
- result
92
- end
93
-
94
- def call_query(query, method, *args)
95
- MutableData.new(query, method, args, @scope)
96
- end
97
- end
98
- end
99
- end
@@ -1,21 +0,0 @@
1
- module Pakyow
2
- module UI
3
- # Adds metadata to a dataset returned by a Mutable query.
4
- #
5
- # @api private
6
- class MutableData
7
- attr_reader :query_name, :query_args, :scope
8
-
9
- def initialize(query, query_name, query_args, scope)
10
- @query = query
11
- @query_name = query_name
12
- @query_args = query_args
13
- @scope = scope
14
- end
15
-
16
- def data
17
- @data ||= @query.call(*@query_args)
18
- end
19
- end
20
- end
21
- end
@@ -1,64 +0,0 @@
1
- require_relative 'channel_builder'
2
-
3
- module Pakyow
4
- module UI
5
- # Provides helper methods to perform in context of a mutation. For example:
6
- #
7
- # view.scope(:foo).mutate(:bar).subscribe
8
- #
9
- # In the above example `mutate` returns a MutateContext object on which
10
- # `subscribe` is called.
11
- #
12
- # @api public
13
- class MutateContext
14
- attr_reader :mutation, :view, :data
15
-
16
- # Creates a new context. Intended to be created by a Mutator.
17
- #
18
- # @api private
19
- def initialize(mutation, view, data)
20
- @mutation = mutation
21
- @view = view
22
- @data = data
23
- end
24
-
25
- # Subscribes a mutation with optional qualifications. Qualifications are
26
- # used to control the scope of future mutations. For example:
27
- #
28
- # view.scope(:foo).mutate(:bar).subscribe(user_id: 1)
29
- #
30
- # In the above example, a subscription is created qualified by `user_id`.
31
- # Only mutations occuring with the same qualifications will cause the
32
- # mutation to be performed again, triggering a view refresh.
33
- #
34
- # ui.mutated(:foo, user_id: 1)
35
- #
36
- # @api public
37
- def subscribe(qualifications = {})
38
- if data.is_a?(MutableData) && !view.context.request.env['pakyow.socket']
39
- MutationStore.instance.register(self, view, data, qualifications, view.context.request.session)
40
- end
41
-
42
- channel = ChannelBuilder.build(
43
- scope: view.scoped_as,
44
- mutation: mutation[:name],
45
- qualifiers: mutation[:qualifiers],
46
- data: data,
47
- qualifications: qualifications
48
- )
49
-
50
- # subscribe to the channel
51
- view.context.socket.subscribe(channel)
52
-
53
- # handle setting the channel on the view
54
- if view.is_a?(Presenter::ViewContext)
55
- working_view = view.instance_variable_get(:@view)
56
- else
57
- working_view = view
58
- end
59
-
60
- working_view.attrs.send(:'data-channel=', channel)
61
- end
62
- end
63
- end
64
- end
@@ -1,38 +0,0 @@
1
- module Pakyow
2
- module UI
3
- # Stores mutations.
4
- #
5
- # @api private
6
- class MutationSet
7
- attr_reader :mutations
8
-
9
- def initialize(&block)
10
- @mutations = {}
11
- instance_exec(&block)
12
- end
13
-
14
- # NOTE I do have some concerns about defining qualifiers in this way;
15
- # mainly because it will lead to having lots of versions of the same
16
- # mutator just so the proper channels will be created.
17
- #
18
- # It's could end up being better to pass qualifiers to `subscribe`;
19
- # however it feels premature to make this decision since it'll lead
20
- # to a large increase in complexity to add at this point.
21
- def mutator(name, qualify: [], &block)
22
- @mutations[name] = {
23
- fn: block,
24
- qualifiers: Array.ensure(qualify),
25
- name: name
26
- }
27
- end
28
-
29
- def mutation(name)
30
- @mutations.fetch(name)
31
- end
32
-
33
- def each(&block)
34
- @mutations.each(&block)
35
- end
36
- end
37
- end
38
- end
@@ -1,41 +0,0 @@
1
- module Pakyow
2
- module UI
3
- # Stores mutations that have occurred in the configured registry.
4
- #
5
- # @api private
6
- class MutationStore
7
- include Singleton
8
-
9
- def initialize
10
- @registry = Config.ui.registry.instance
11
- end
12
-
13
- def register(mutate_context, view, mutable_data, qualifications, session)
14
- @registry.register(
15
- mutable_data.scope,
16
-
17
- view_scope: view.scoped_as,
18
- mutation: mutate_context.mutation[:name],
19
- qualifiers: mutate_context.mutation[:qualifiers],
20
- qualifications: qualifications,
21
- query_name: mutable_data.query_name,
22
- query_args: mutable_data.query_args,
23
- session: session.to_hash,
24
- socket_key: mutate_context.view.context.socket_digest(mutate_context.view.context.socket_connection_id)
25
- )
26
- end
27
-
28
- def unregister(socket_key)
29
- @registry.unregister(socket_key)
30
- end
31
-
32
- def mutations(scope)
33
- @registry.mutations(scope) || []
34
- end
35
- end
36
- end
37
- end
38
-
39
- Pakyow::Realtime::Websocket.on :leave do
40
- Pakyow::UI::MutationStore.instance.unregister(socket_digest(socket_connection_id))
41
- end
@@ -1,63 +0,0 @@
1
- require_relative 'mutation_set'
2
- require_relative 'mutate_context'
3
-
4
- module Pakyow
5
- module UI
6
- # Performs mutations on views.
7
- #
8
- # @api private
9
- class Mutator
10
- include Singleton
11
-
12
- attr_reader :sets
13
-
14
- # @api private
15
- def initialize
16
- reset
17
- end
18
-
19
- def reset
20
- @sets = {}
21
- @mutables = {}
22
- self
23
- end
24
-
25
- def set(scope, &block)
26
- @sets[scope] = MutationSet.new(&block)
27
- end
28
-
29
- def mutable(scope, context = nil, &block)
30
- if block_given?
31
- @mutables[scope] = block
32
- else
33
- # TODO: inefficient to have to execute the block each time
34
- Mutable.new(context, scope, &@mutables[scope])
35
- end
36
- end
37
-
38
- def mutation(scope, name)
39
- if mutations = mutations_by_scope(scope)
40
- mutations.mutation(name)
41
- end
42
- end
43
-
44
- # TODO: rename to mutation_set_for_scope
45
- def mutations_by_scope(scope)
46
- @sets[scope]
47
- end
48
-
49
- def mutate(mutation_name, view, data)
50
- if mutation = mutation(view.scoped_as, mutation_name)
51
- if data.is_a?(MutableData)
52
- working_data = data.data
53
- else
54
- working_data = data
55
- end
56
-
57
- view.instance_exec(view, working_data, &mutation[:fn])
58
- MutateContext.new(mutation, view, data)
59
- end
60
- end
61
- end
62
- end
63
- end
@@ -1,87 +0,0 @@
1
- require_relative 'mock_mutation_eval'
2
-
3
- module Pakyow
4
- module Presenter
5
- # Stands in for a real View object and makes any attempted transformation
6
- # a no-op.
7
- #
8
- # @api private
9
- class NoOpView
10
- include Helpers
11
- VIEW_CLASSES = [ViewContext]
12
-
13
- # The arities of misc view methods that switch the behavior from
14
- # instance_exec to yield.
15
- #
16
- EXEC_ARITIES = { with: 0, for: 1, for_with_index: 2, repeat: 1,
17
- repeat_with_index: 2, bind: 1, bind_with_index: 2,
18
- apply: 1 }
19
-
20
- def initialize(view, context)
21
- @view = view
22
- @context = context
23
- end
24
-
25
- def is_a?(klass)
26
- @view.is_a?(klass)
27
- end
28
-
29
- # View methods that should be a no-op
30
- #
31
- %i(bind bind_with_index apply).each do |method|
32
- define_method(method) do |_data, **_kargs, &_block|
33
- self
34
- end
35
- end
36
-
37
- def mutate(mutator, with: nil, data: nil)
38
- MockMutationEval.new(mutator, with || data, self)
39
- end
40
-
41
- # Pass these through, handling the return value.
42
- #
43
- def method_missing(method, *args, &block)
44
- ret = @view.send(method, *args, &wrap(method, &block))
45
- handle_return_value(ret)
46
- end
47
-
48
- private
49
-
50
- def view?(obj)
51
- VIEW_CLASSES.include?(obj.class)
52
- end
53
-
54
- # Returns a new context for returned views, or the return value.
55
- #
56
- def handle_return_value(value)
57
- return NoOpView.new(value, @context) if view?(value)
58
-
59
- value
60
- end
61
-
62
- # Wrap the block, substituting the view with the current view context.
63
- #
64
- def wrap(method, &block)
65
- return if block.nil?
66
-
67
- proc do |*args|
68
- ctx = args.map! { |arg|
69
- view?(arg) ? NoOpView.new(arg, @context) : arg
70
- }.find { |arg| arg.is_a?(ViewContext) }
71
-
72
- case block.arity
73
- when EXEC_ARITIES[method]
74
- # Rejecting ViewContext handles the edge cases around the order of
75
- # arguments from view methods (since view is not present in some
76
- # situations and when it is present, is always the first arg).
77
- ctx.instance_exec(*args.reject { |arg|
78
- arg.is_a?(ViewContext)
79
- }, &block)
80
- else
81
- block.call(*args)
82
- end
83
- end
84
- end
85
- end
86
- end
87
- end
@@ -1,70 +0,0 @@
1
- require 'json'
2
-
3
- module Pakyow
4
- module UI
5
- # Manages mutations.
6
- #
7
- # This is the default registry in production systems and is required in
8
- # deployments with more than one app instance.
9
- #
10
- # @api private
11
- class RedisMutationRegistry
12
- include Singleton
13
-
14
- def initialize
15
- end
16
-
17
- def register(scope, mutation)
18
- Pakyow::Realtime.redis.sadd(key(scope: scope, socket_key: mutation[:socket_key]), mutation.to_json)
19
- end
20
-
21
- def mutations(scope)
22
- mutations = []
23
-
24
- keys(key(scope: scope)) do |key|
25
- Pakyow::Realtime.redis.smembers(key).each do |m|
26
- mutations << Hash.strhash(JSON.parse(m))
27
- end
28
- end
29
-
30
- mutations
31
- end
32
-
33
- def unregister(socket_key)
34
- keys(key(socket_key: socket_key)) do |key|
35
- Pakyow::Realtime.redis.del(key)
36
- end
37
- end
38
-
39
- private
40
-
41
- def key(scope: nil, socket_key: nil)
42
- if socket_key.nil?
43
- base = "*:"
44
- else
45
- base = "#{socket_key}:"
46
- end
47
-
48
- if scope.nil?
49
- "#{base}*"
50
- else
51
- "#{base}pui-mutation-#{scope}"
52
- end
53
- end
54
-
55
- def keys(match)
56
- cursor = 0
57
-
58
- loop do
59
- cursor, keys = Pakyow::Realtime.redis.scan(cursor, match: match)
60
-
61
- keys.each do |key|
62
- yield key
63
- end
64
-
65
- break if cursor == '0'
66
- end
67
- end
68
- end
69
- end
70
- end
@@ -1,37 +0,0 @@
1
- module Pakyow
2
- module UI
3
- # Manages mutations.
4
- #
5
- # Intended only for use in development or single app-instance deployments.
6
- #
7
- # @api private
8
- class SimpleMutationRegistry
9
- include Singleton
10
-
11
- def initialize
12
- reset
13
- end
14
-
15
- def reset
16
- @mutations = {}
17
- end
18
-
19
- def register(scope, mutation)
20
- @mutations[scope] ||= []
21
- @mutations[scope] << mutation
22
- end
23
-
24
- def unregister(socket_key)
25
- @mutations.each do |_, mutations|
26
- mutations.delete_if { |mutation|
27
- mutation[:socket_key] == socket_key
28
- }
29
- end
30
- end
31
-
32
- def mutations(scope)
33
- @mutations[scope]
34
- end
35
- end
36
- end
37
- end