flok 0.0.100 → 0.0.101

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/app/drivers/chrome/README.md +6 -0
  3. data/app/drivers/chrome/Rakefile +1 -0
  4. data/app/drivers/chrome/build_context.rb +1 -0
  5. data/app/drivers/chrome/config.yml +4 -0
  6. data/app/drivers/chrome/spec/spec/dlink_spec.js +15 -0
  7. data/app/drivers/chrome/spec/spec/hook_spec.js +24 -0
  8. data/app/drivers/chrome/src/dlink.js +28 -0
  9. data/app/drivers/chrome/src/hook.js +29 -0
  10. data/app/kern/mod/dlink.js +16 -0
  11. data/app/kern/mod/event.js +7 -0
  12. data/app/kern/mod/hook.js +1 -0
  13. data/app/kern/services/dlink.rb +25 -0
  14. data/bin/flok +12 -3
  15. data/docs/compilation.md +1 -1
  16. data/docs/kernel_api.md +1 -1
  17. data/docs/kernel_handbook/hooks.md +4 -0
  18. data/docs/mod/dlink.md +18 -0
  19. data/docs/mod/hook.md +9 -0
  20. data/docs/project.md +3 -2
  21. data/docs/services/dlink.md +8 -0
  22. data/docs/user_handbook/hooks.md +106 -0
  23. data/lib/flok.rb +1 -0
  24. data/lib/flok/hooks_compiler.rb +174 -0
  25. data/lib/flok/project_template/config/hooks.rb +1 -0
  26. data/lib/flok/user_compiler.rb +56 -6
  27. data/lib/flok/user_compiler_templates/ctable.js.erb +4 -0
  28. data/lib/flok/user_hook_generators/goto.rb +74 -0
  29. data/lib/flok/user_hook_generators/helpers.rb +46 -0
  30. data/lib/flok/version.rb +1 -1
  31. data/spec/env/kern.rb +32 -16
  32. data/spec/etc/transition_complier_spec.rb +4 -0
  33. data/spec/iface/driver/hook_spec.rb +33 -0
  34. data/spec/iface/kern/dlink_spec.rb +25 -0
  35. data/spec/kern/assets/dlink_service/config0.rb +2 -0
  36. data/spec/kern/assets/dlink_service/controller0.rb +16 -0
  37. data/spec/kern/assets/hook_entry_points/controller0.rb +58 -0
  38. data/spec/kern/assets/hook_entry_points/controller0a.rb +27 -0
  39. data/spec/kern/dlink_service_spec.rb +50 -0
  40. data/spec/kern/hook_entry_points_and_manifest_spec.rb +155 -0
  41. data/spec/kern/hook_user_generators_spec.rb +139 -0
  42. metadata +36 -3
  43. data/docs/mod/intercept.md +0 -26
data/lib/flok.rb CHANGED
@@ -9,6 +9,7 @@ require "flok/interactive"
9
9
  require "flok/user_compiler"
10
10
  require "flok/services_compiler"
11
11
  require "flok/transition_compiler"
12
+ require "flok/hooks_compiler"
12
13
 
13
14
  module Flok
14
15
  end
@@ -0,0 +1,174 @@
1
+ require_relative './macro.rb'
2
+ #The hooks compiler is a late-stage (see ./project.md) compiler that takes the almost-fully-compiled
3
+ #javascript source (which includes the user's javascript controllers by now) and looks for special
4
+ #comment markers for which it can inject code.
5
+ module Flok
6
+ module HooksCompiler
7
+ #Returns a new copy of the source transformed as described by the manifest
8
+ def self.compile(src, manifest)
9
+ new_src = src.split("\n").map{|e| manifest.transform_line(e) }.join("\n")
10
+
11
+ #Re-process macros
12
+ new_src = Flok.macro_process new_src
13
+
14
+
15
+ return new_src
16
+ end
17
+ end
18
+
19
+ #A hooks manifest contains all the information needed so that the hooks compiler
20
+ #can find can change the code
21
+ class HooksManifest
22
+ def initialize
23
+ @manifest_entries = []
24
+ end
25
+
26
+ #Returns a copy of the line with transformations if needed. For example, if a line contains
27
+ #a hook entry point, like HOOK_ENTRY[my_event] and the manifest contains code that should
28
+ #be inserted there, this will return the inserted code (which may then be multiple lines)
29
+ #And will also remove the comment itself
30
+ def transform_line line
31
+ #Get all the matched HooksManifestEntry(s)
32
+ injected_code = @manifest_entries.select{|e| e.does_match? line}.map{|e| e.code_for_line(line)}
33
+
34
+ #If there is a match of at least one hook, remove the original line and replace it
35
+ #with all the newly found code from the HooksManifestEntry(s)
36
+ return injected_code.join("\n") if injected_code.count > 0
37
+
38
+ #Else, nothing was found, keep moving along and don't transform the line
39
+ return line
40
+ end
41
+
42
+ #Accepts a HooksManifestEntry which can match a line and then return some text
43
+ #that should be apart of that line. Multiple matching manifest entries
44
+ #is possible
45
+ def <<(entry)
46
+ @manifest_entries << entry
47
+ end
48
+ end
49
+
50
+ #When HooksManifest goes line by line, a line is considered matching when this entry
51
+ #retruns true for its does_match? function. You pass it a name, optional parameter
52
+ #query and the block to call when the code should be generated for a match. The block
53
+ #receives a full copy of the parameters for the hook entry (the static ones). The
54
+ #param query is a lambda that also receives the block and it should return (next) true/false
55
+ #when a match is considered true. If name is just "*" (string, not symbol), then it matches
56
+ #all names. Passing multiple param_queries in is allowed, you just pass in multiple arrays.
57
+ #All of the procs passed must be true for the result to be true
58
+ #
59
+ #E.g. - This would match //HOOK_ENTRY[my_event]
60
+ #>HooksManifestEntry.new("my_event", ->(p){p["actions"].include? "x"}) do |hook_info|
61
+ # return "console.log(info = #{info["actions"]});"
62
+ #end
63
+ class HooksManifestEntry
64
+ def initialize name, param_queries=->(p){true}, &block
65
+ #If an array is not passed, make it an array with one element
66
+ param_queries = [param_queries] if param_queries.class != Array
67
+
68
+ @name = name.to_s
69
+ @param_queries = param_queries
70
+ @block = block
71
+ end
72
+
73
+ def does_match? line
74
+ #Unless the matching name is a *, check to see if the hook name matches
75
+ unless @name == "*"
76
+ return false unless line =~ /\/\/HOOK_ENTRY\[#{@name}\]/
77
+ end
78
+
79
+ #Now verify with the lambda
80
+ return @param_queries.reduce(true){|r, e| r &&= e.call(hook_entry_info_from_line(line))}
81
+ end
82
+
83
+ def code_for_line line
84
+ return @block.call(hook_entry_info_from_line(line))
85
+ end
86
+
87
+ #Each hook entry contains a JSON encoded set of static parameters
88
+ #for things like the controller name, etc. See docs for a list of
89
+ #parameters as it depends on the hook entry
90
+ def hook_entry_info_from_line line
91
+ json_info = line.split(/\/\/HOOK_ENTRY\[.*?\]/).last.strip
92
+ begin
93
+ return JSON.parse(json_info)
94
+ rescue => e
95
+ raise "Couldn't parse the hooks entry JSON information, got #{json_info.inspect}: #{e.inspect}"
96
+ end
97
+ end
98
+ end
99
+
100
+ #This converts all the user hooks into a manifest
101
+ module UserHooksToManifestOrchestrator
102
+ $generators = {}
103
+
104
+ #Register a user hook generator to be available to the user in their `./config/hooks.rb`
105
+ #The name is what is used to find the correct generator when a user uses the
106
+ #hook :name DSL syntax
107
+ def self.register_hook_gen name, &gen_block
108
+ $generators[name] = gen_block
109
+ end
110
+
111
+ #Converts the `./config/hooks.rb` of the user into a HooksManifestEntry
112
+ def self.convert_hooks_to_manifest hooks_rb
113
+ #Create a new manifest, this will be passed to each generator instance
114
+ #along with a set of parameters. Each generator will update the
115
+ #hooks manifest
116
+ manifest = HooksManifest.new
117
+
118
+ #Evaluate the user hooks DSL which will create a listing of all the
119
+ #user's requests in the accessible :hooks_requests member of the dsl environment
120
+ hooks_dsl_env = UserHooksDSL.new
121
+ hooks_dsl_env.instance_eval hooks_rb
122
+
123
+ #For each user request, lookup the appropriate generator handler and then
124
+ #call the generator
125
+ hook_requests = hooks_dsl_env.hook_requests
126
+ hook_requests.each do |gen_name, requests|
127
+ generator = $generators[gen_name]
128
+ raise "A hook request requested the generator by the name #{gen_name.inspect} but this was not a generator..." unless generator
129
+
130
+ requests.each do |r|
131
+ generator.call(manifest, r)
132
+ end
133
+ end
134
+
135
+ return manifest
136
+ end
137
+
138
+ #Interpret `./config/hooks.rb` to call the associated registered hook gen block
139
+ class UserHooksDSL
140
+ attr_accessor :hook_requests
141
+ def initialize
142
+ #Each user hook call adds a request to this hash
143
+ #which is then processed by each matching hook generator
144
+ @hook_requests = {}
145
+ end
146
+
147
+ #Install a hook. Leads to eventually calling the relavent hook generator. The anmes argument
148
+ #takes one key-value pair e.g. {:goto => :settings_changed}, this would mean the hook generator
149
+ #named 'goto' and it should create a hook event called 'settings_changed'. The block
150
+ #is then passed on to each hook generator. See the docs on hooks.md for information on what
151
+ #each block function takes
152
+ def hook names, &block
153
+
154
+ #The names parameter
155
+ key = names.keys.first
156
+ hook_event_name = names.values.first
157
+ raise "You didn't supply a hook generator name or the event name... Got #{key.inspect} and #{hook_event_name.inspect}. e.g. hook :goto => :changed, {...}" unless key and hook_event_name
158
+
159
+ @hook_requests[key] ||= []
160
+ @hook_requests[key] << {
161
+ :hook_event_name => hook_event_name,
162
+ :block => block
163
+ }
164
+ end
165
+ end
166
+ end
167
+ end
168
+
169
+ #Load all the user hook generators
170
+ Dir.chdir File.join(File.dirname(__FILE__), "../../") do
171
+ Dir["./lib/flok/user_hook_generators/*"].each do |f|
172
+ load f
173
+ end
174
+ end
@@ -0,0 +1 @@
1
+ #Hooks stub
@@ -22,7 +22,7 @@ module Flok
22
22
  end
23
23
  end
24
24
 
25
- #Compiler executes all rb code inside this context
25
+ #Compiler executes all rb code (ERB) inside this context
26
26
  module Flok
27
27
  class UserCompilerContext
28
28
  attr_accessor :controllers, :actions, :ons
@@ -31,6 +31,21 @@ module Flok
31
31
  @controllers = []
32
32
  @actions = []
33
33
  @ons = []
34
+
35
+ @debug = ENV["FLOK_ENV"] ? true : false
36
+ end
37
+
38
+ #Returns a list of events that this controller 'might' respond to
39
+ #Used for things like hook event handlers to provide queryable
40
+ #information.
41
+ def might_respond_to
42
+ @actions.map{|e| e.ons}.flatten.map{|e| e[:name]}
43
+ end
44
+
45
+ #actions_responds_to looks like {"action1" => ["event_a", ..."], "action2" => }...
46
+ #where each action list contains all the events this action responds to
47
+ def actions_respond_to
48
+ @actions.map{|e| [e.name.to_s, e.ons.map{|e| e[:name].to_s}]}.to_h
34
49
  end
35
50
 
36
51
  def get_binding
@@ -85,8 +100,18 @@ module Flok
85
100
 
86
101
  #Calculate spot index as an offset from the base address using the index of the spot in the spots
87
102
  #address offset
88
- res = %{
89
-
103
+ res = ""
104
+
105
+ if @debug
106
+ res += %{
107
+ }
108
+ end
109
+
110
+ res += %{
111
+ <% if @debug %>
112
+ if (__base__.constructor !== Number) { throw "Embed for the controller: #{@controller.name} was not given a number for it's __base__ pointer, but of type: " + __base__.constructor + "with the value: " + __base__};
113
+ <% end %>
114
+
90
115
  var ptr = _embed("#{vc_name}", __base__+#{spot_index}+1, #{context}, __base__);
91
116
  __info__.embeds[#{spot_index-1}].push(ptr);
92
117
  }
@@ -158,6 +183,8 @@ module Flok
158
183
  var old_action = __info__.action;
159
184
  __info__.action = "#{action_name}";
160
185
 
186
+ //HOOK_ENTRY[controller_will_goto] #{{"controller_name" => @controller.name, "might_respond_to" => @ctx.might_respond_to, "actions_responds_to" => @ctx.actions_respond_to, "from_action" => @name, "to_action" => action_name}.to_json}
187
+
161
188
  //Remove all views, we don't have to recurse because removal of a view
162
189
  //is supposed to remove *all* view controllers of that tree as well.
163
190
  var embeds = __info__.embeds;
@@ -494,7 +521,7 @@ module Flok
494
521
  end
495
522
 
496
523
  class UserCompilerAction
497
- attr_accessor :controller, :name, :ons, :every_handlers
524
+ attr_accessor :controller, :name, :every_handlers
498
525
  include UserCompilerMacro
499
526
 
500
527
  def initialize controller, name, ctx, &block
@@ -502,7 +529,7 @@ module Flok
502
529
  @name = name
503
530
  @ctx = ctx
504
531
  @_on_entry_src = ""
505
- @ons = [] #Event handlers
532
+ @_ons = [] #Event handlers
506
533
  @every_handlers = []
507
534
 
508
535
  self.instance_eval(&block)
@@ -518,7 +545,30 @@ module Flok
518
545
  end
519
546
 
520
547
  def on name, js_src
521
- @ons << {:name => name, :src => _macro(js_src)}
548
+ #We need this guard because we run a two pass compile on the ons. When 'ons' is accessed, it is assumed that we are now
549
+ #in the compilation phase and we build all the entries. This is because some macros in the ons source code requires
550
+ #prior-knowledge of controller-level information like all possible events in all actions for hooks
551
+ raise "Uh oh, you tried to add an event handler but we already assumed that compilation took place so we cached everything..." if @__ons_did_build or @__ons_is_building
552
+
553
+ @_ons << {:name => name, :src => js_src}
554
+ end
555
+
556
+ def ons
557
+ #Return the un-compiled version as some macros access this data and the real ons
558
+ #would cause infinite recursion
559
+ return @_ons if @__ons_is_building
560
+ @__ons_is_building = true
561
+
562
+ #We need this guard because we run a two pass compile on the ons. When 'ons' is accessed, it is assumed that we are now
563
+ #in the compilation phase and we build all the entries. This is because some macros in the ons source code requires
564
+ #prior-knowledge of controller-level information like all possible events in all actions for hooks
565
+ unless @__ons_did_build
566
+ @__ons_did_build = true
567
+ @__ons = @_ons.map{|e| {:name => e[:name], :src => _macro(e[:src])}}
568
+ end
569
+
570
+ @__ons_is_building = false
571
+ return @__ons
522
572
  end
523
573
 
524
574
  def every seconds, str
@@ -69,6 +69,10 @@ ctable = {
69
69
  handlers: {
70
70
  <% a.ons.each do |e| %>
71
71
  <%= e[:name] %>: function(__base__, params) {
72
+ <% if @debug %>
73
+ if (__base__.constructor !== Number) { throw "on('<%= e[:name] %>') for the controller: <%= c.name %>:<%= a.name %> was not given a number for it's __base__ pointer, but of type: " + __base__.constructor + "with the value: " + __base__};
74
+ <% end %>
75
+
72
76
  var __info__ = tel_deref(__base__);
73
77
  var context = __info__.context;
74
78
  var current_action = __info__.action;
@@ -0,0 +1,74 @@
1
+ require_relative 'helpers'
2
+
3
+ module Flok
4
+ class GotoHooksDSLEnv
5
+ attr_accessor :selectors
6
+
7
+ def initialize
8
+ @selectors = []
9
+ end
10
+
11
+ def controller name
12
+ @selectors << ->(p) { p["controller_name"] and p["controller_name"] == name.to_s }
13
+ end
14
+
15
+ #The previous / next action contains an event handler for...
16
+ #################################################################################
17
+ def from_action_responds_to? responds
18
+ @selectors << lambda do |params|
19
+ from_action = params["from_action"]
20
+ actions_respond_to = params["actions_responds_to"] #This is a hash that maps all actions to sensetivity lists
21
+
22
+ #Get the sensetivity list if possible for this action (this is the list of events this action responds to)
23
+ if actions_respond_to[from_action]
24
+ sensetivity_list = actions_respond_to[from_action]
25
+
26
+ #Does the sensetivity list include the event we are interested in?
27
+ next sensetivity_list.include? responds
28
+ end
29
+
30
+ #The action wasn't even listed on the list, i.e. it has no sensetivity list
31
+ next false
32
+ end
33
+ end
34
+
35
+ def to_action_responds_to? responds
36
+ @selectors << lambda do |params|
37
+ to_action = params["to_action"]
38
+ actions_respond_to = params["actions_responds_to"] #This is a hash that maps all actions to sensetivity lists
39
+
40
+ #Get the sensetivity list if possible for this action (this is the list of events this action responds to)
41
+ if actions_respond_to[to_action]
42
+ sensetivity_list = actions_respond_to[to_action]
43
+
44
+ #Does the sensetivity list include the event we are interested in?
45
+ next sensetivity_list.include? responds
46
+ end
47
+
48
+ #The action wasn't even listed on the list, i.e. it has no sensetivity list
49
+ next false
50
+ end
51
+ end
52
+ #################################################################################
53
+ end
54
+
55
+ UserHooksToManifestOrchestrator.register_hook_gen :goto do |manifest, params|
56
+ hook_event_name = params[:hook_event_name]
57
+
58
+ #Evaluate User given DSL (params[:block]) which comes from `./confg/hooks.rb`
59
+ #to retrieve a set of selectors which we will pass the hooks compiler
60
+ block = params[:block]
61
+ dsl_env = GotoHooksDSLEnv.new
62
+ dsl_env.instance_eval(&block)
63
+
64
+ #Inject into HOOK_ENTRY[controller_will_goto] that match the given selectors from the DSL
65
+ #based on the hook entry static parameters
66
+ entry = HooksManifestEntry.new("controller_will_goto", dsl_env.selectors) do |entry_hook_params|
67
+ next %{
68
+ SEND("main", "if_hook_event", "#{hook_event_name}", {});
69
+ }
70
+ end
71
+
72
+ manifest << entry
73
+ end
74
+ end
@@ -0,0 +1,46 @@
1
+ module Flok
2
+ #This class helps construct javascript expressions like ((a == 3 || b == 4) && (a == 5 || c == 6))
3
+ #Usually you have an outer-level JSTermGroup that represents all the things being anded togeather
4
+ #and then you have multiple inner groups that each represent ored terms. Search POS form on wikipedia
5
+ #e.g.
6
+ # #Convert ((a == 3 || b == 4) && (a == 5 || c == 6))
7
+ #
8
+ # This will hold the entire result
9
+ # ands = JSTermGroup.new
10
+ #
11
+ # Compute a small section of the result, the group (a == 3 || b == 4) and
12
+ # then add it as a group of the entire result in || form
13
+ # ors1 = JSTermGroup.new
14
+ # ors1 << "a == 3"
15
+ # ors1 << "b == 4"
16
+ # ands << ors1.to_or_js
17
+ #
18
+ # Same thing but now we compute the second part which is (a == 5 || c == 5)
19
+ # ors2 = JSTermGroup.new
20
+ # ors2 << "a == 5"
21
+ # ors2 << "c == 6"
22
+ # ands << ors2.to_or_js
23
+ #
24
+ # Finally get the result by &&ing all the ||s groups togeather
25
+ # result = ands.to_and_js
26
+ class JSTermGroup
27
+ def initialize
28
+ @terms = []
29
+ end
30
+
31
+ #Add a javascript expression that evaluates to either true or false. May or may not contain parantheses
32
+ def << expr
33
+ @terms << "(#{expr})"
34
+ end
35
+
36
+ #Join expressions via || statements and yield an expression that contains parantheses
37
+ def to_or_js
38
+ "(#{[*@terms, "false"].join(" || ")})"
39
+ end
40
+
41
+ #Join expressions via && statements and yield an expression that contains parantheses
42
+ def to_and_js
43
+ "(#{[*@terms, "true"].join(" && ")})"
44
+ end
45
+ end
46
+ end
data/lib/flok/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Flok
2
- VERSION = "0.0.100"
2
+ VERSION = "0.0.101"
3
3
  end
data/spec/env/kern.rb CHANGED
@@ -69,7 +69,32 @@ shared_context "kern" do
69
69
 
70
70
  #Create a new flok project, add the given user_file (an .rb file containing controllers, etc.)
71
71
  #and then retrieve a V8 instance from this project's application_user.js
72
- def flok_new_user user_controllers_src, service_config=nil, service_src=nil
72
+ def flok_new_user user_controllers_src, service_config=nil, service_src=nil, hooks_src=nil
73
+ return flok_new_user_with_src(user_controllers_src, service_config, service_src, hooks_src)[:ctx]
74
+ end
75
+
76
+ #Returns a v8 instance with modifications like handling dispatch of flok
77
+ #And sets some members (not the greatest)
78
+ def v8_flok
79
+ #Execute
80
+ @driver = FakeDriverContext.new
81
+ v8 = V8::Context.new(:with => @driver)
82
+ @ctx = v8
83
+ @driver.ctx = v8
84
+ v8.eval %{
85
+ //We must convert this to JSON because the fake driver will receive
86
+ //a raw v8 object otherwise
87
+ function if_dispatch(q) {
88
+ if_dispatch_json(JSON.stringify(q));
89
+ }
90
+ }
91
+
92
+ return v8
93
+ end
94
+
95
+ #Create a new flok project, add the given user_file (an .rb file containing controllers, etc.)
96
+ #and then retrieve a V8 instance from this project's application_user.js
97
+ def flok_new_user_with_src user_controllers_src, service_config=nil, service_src=nil, hooks_src=nil
73
98
  temp_dir = new_temp_dir
74
99
  Dir.chdir temp_dir do
75
100
  flok "new test"
@@ -78,31 +103,22 @@ shared_context "kern" do
78
103
  File.write './app/controllers/user_controller.rb', user_controllers_src
79
104
  File.write './config/services.rb', service_config if service_config
80
105
  File.write './app/services/service0.rb', service_src if service_src
106
+ File.write './config/hooks.rb', hooks_src if hooks_src
81
107
 
82
108
  #Build
83
109
  unless flok "build" #Will generate drivers/ but we will ignore that
84
110
  raise "Build failed"
85
111
  end
86
112
 
87
- #Execute
88
- @driver = FakeDriverContext.new
89
- v8 = V8::Context.new(:with => @driver)
90
- @ctx = v8
91
- @driver.ctx = v8
92
- v8.eval %{
93
- //We must convert this to JSON because the fake driver will receive
94
- //a raw v8 object otherwise
95
- function if_dispatch(q) {
96
- if_dispatch_json(JSON.stringify(q));
97
- }
98
- }
99
-
100
- v8.eval File.read('./products/chrome/application_user.js')
101
- return v8
113
+ v8 = v8_flok
114
+ src = File.read('./products/chrome/application_user.js')
115
+ v8.eval src
116
+ return {:ctx => v8, :src => src}
102
117
  end
103
118
  end
104
119
  end
105
120
 
121
+
106
122
  #This supports if_dispatch interface and allows for sending information back via
107
123
  #int_dispatch to the kernel. It is embededd into the v8 context environment
108
124
  class FakeDriverContext