pakyow-ui 0.11.3 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
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