flok 0.0.100 → 0.0.101

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