flex 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/LICENSE +20 -0
  2. data/README.md +20 -0
  3. data/VERSION +1 -0
  4. data/flex.gemspec +43 -0
  5. data/lib/flex.rb +418 -0
  6. data/lib/flex/api_methods.yml +108 -0
  7. data/lib/flex/class_proxy.rb +12 -0
  8. data/lib/flex/configuration.rb +57 -0
  9. data/lib/flex/errors.rb +42 -0
  10. data/lib/flex/http_clients/patron.rb +27 -0
  11. data/lib/flex/http_clients/rest_client.rb +38 -0
  12. data/lib/flex/loader.rb +116 -0
  13. data/lib/flex/logger.rb +16 -0
  14. data/lib/flex/model.rb +24 -0
  15. data/lib/flex/model/class_proxy.rb +45 -0
  16. data/lib/flex/model/instance_proxy.rb +101 -0
  17. data/lib/flex/model/manager.rb +67 -0
  18. data/lib/flex/rails.rb +12 -0
  19. data/lib/flex/rails/engine.rb +23 -0
  20. data/lib/flex/rails/helper.rb +16 -0
  21. data/lib/flex/related_model.rb +16 -0
  22. data/lib/flex/related_model/class_proxy.rb +23 -0
  23. data/lib/flex/related_model/class_sync.rb +23 -0
  24. data/lib/flex/related_model/instance_proxy.rb +28 -0
  25. data/lib/flex/result.rb +18 -0
  26. data/lib/flex/result/bulk.rb +20 -0
  27. data/lib/flex/result/collection.rb +51 -0
  28. data/lib/flex/result/document.rb +38 -0
  29. data/lib/flex/result/indifferent_access.rb +11 -0
  30. data/lib/flex/result/search.rb +51 -0
  31. data/lib/flex/result/source_document.rb +63 -0
  32. data/lib/flex/result/source_search.rb +32 -0
  33. data/lib/flex/structure/indifferent_access.rb +44 -0
  34. data/lib/flex/structure/mergeable.rb +21 -0
  35. data/lib/flex/tasks.rb +141 -0
  36. data/lib/flex/template.rb +187 -0
  37. data/lib/flex/template/base.rb +29 -0
  38. data/lib/flex/template/info.rb +50 -0
  39. data/lib/flex/template/partial.rb +31 -0
  40. data/lib/flex/template/search.rb +30 -0
  41. data/lib/flex/template/slim_search.rb +13 -0
  42. data/lib/flex/template/tags.rb +46 -0
  43. data/lib/flex/utility_methods.rb +140 -0
  44. data/lib/flex/utils.rb +59 -0
  45. data/lib/flex/variables.rb +11 -0
  46. data/lib/generators/flex/setup/setup_generator.rb +51 -0
  47. data/lib/generators/flex/setup/templates/flex_config.yml +16 -0
  48. data/lib/generators/flex/setup/templates/flex_dir/es.rb.erb +18 -0
  49. data/lib/generators/flex/setup/templates/flex_dir/es.yml.erb +19 -0
  50. data/lib/generators/flex/setup/templates/flex_dir/es_extender.rb.erb +17 -0
  51. data/lib/generators/flex/setup/templates/flex_initializer.rb.erb +44 -0
  52. data/lib/tasks/index.rake +23 -0
  53. data/test/flex.irt +143 -0
  54. data/test/flex/configuration.irt +53 -0
  55. data/test/irt_helper.rb +12 -0
  56. metadata +211 -0
@@ -0,0 +1,29 @@
1
+ module Flex
2
+ class Template
3
+ module Base
4
+
5
+ def process_vars(vars)
6
+ missing = @tags - vars.keys
7
+ raise ArgumentError, "required variables #{missing.inspect} missing." \
8
+ unless missing.empty?
9
+ @partials.each do |k|
10
+ raise MissingPartialError, "undefined #{k} partial template" \
11
+ unless @host_flex.partials.has_key?(k)
12
+ next if vars[k].nil?
13
+ vars[k] = [vars[k]] unless vars[k].is_a?(Array)
14
+ vars[k] = vars[k].map {|v| @host_flex.partials[k].interpolate(@variables.deep_dup, v)}
15
+ end
16
+ vars[:index] = vars[:index].join(',') if vars[:index].is_a?(Array)
17
+ vars[:type] = vars[:type].join(',') if vars[:type].is_a?(Array)
18
+ if vars[:page]
19
+ vars[:params] ||= {}
20
+ page = vars[:page].to_i
21
+ page = 1 unless page > 0
22
+ vars[:params][:from] = ((page - 1) * vars[:params][:size] || vars[:size] || 10).ceil
23
+ end
24
+ vars
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,50 @@
1
+ module Flex
2
+ class Template
3
+ module Info
4
+
5
+ def info(*names)
6
+ names = templates.keys if names.empty?
7
+ info = "\n"
8
+ names.each do |name|
9
+ next unless templates.include?(name)
10
+ block = ''
11
+ temp = templates[name]
12
+ meth_call = [host_class, name].join('.')
13
+ block << "########## #{meth_call} ##########\n\n#{'-' * temp.class.to_s.length}\n#{temp.class}\n#{temp.to_flex(name)}\n"
14
+ temp.partials.each do |par_name|
15
+ par = partials[par_name]
16
+ block << "#{'-' * par.class.to_s.length}\n#{par.class}\n#{par.to_flex(par_name)}\n"
17
+ end
18
+ block << "\nUsage:\n"
19
+ block << usage(meth_call, temp)
20
+ block << "\n "
21
+ info << block.split("\n").map{|l| '# ' + l}.join("\n")
22
+ info << "\ndef #{meth_call}(vars={})\n # this is a stub, used for reference\nend\n\n\n"
23
+ end
24
+ info
25
+ end
26
+
27
+ private
28
+
29
+ def usage(meth_call, temp)
30
+ all_tags = temp.tags + temp.partials
31
+ lines = all_tags.map do |t|
32
+ comments = 'partial' if t.to_s.match(/^_/)
33
+ ['', t.to_s] + (temp.variables.has_key?(t) ? ["#{temp.variables[t].inspect},", comments_to_s(comments)] : ["#{t},", comments_to_s(comments, 'required')])
34
+ end
35
+ lines.sort! { |a,b| b[3] <=> a[3] }
36
+ lines.first[0] = meth_call
37
+ lines.last[2].chop!
38
+ max = lines.transpose.map { |c| c.map(&:length).max }
39
+ lines.map { |line| "%-#{max[0]}s :%-#{max[1]}s => %-#{max[2]}s %s" % line }.join("\n")
40
+ end
41
+
42
+ def comments_to_s(*comments)
43
+ comments = comments.compact
44
+ return '' if comments == []
45
+ "# #{comments.join(' ')}"
46
+ end
47
+
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,31 @@
1
+ module Flex
2
+ class Template
3
+ class Partial
4
+
5
+ include Base
6
+
7
+ def initialize(data, parent)
8
+ @data = data
9
+ @parent = parent
10
+ tags = Tags.new
11
+ stringified = tags.stringify(data)
12
+ @partials, @tags = tags.map(&:name).partition{|n| n.to_s =~ /^_/}
13
+ @variables = tags.variables
14
+ instance_eval <<-ruby, __FILE__, __LINE__
15
+ def interpolate(main_vars=Variables.new, vars={})
16
+ sym_vars = {}
17
+ vars.each{|k,v| sym_vars[k.to_sym] = v} # so you can pass the rails params hash
18
+ main_vars.add(@variables, sym_vars)
19
+ vars = process_vars(main_vars)
20
+ #{stringified}
21
+ end
22
+ ruby
23
+ end
24
+
25
+ def to_flex(name=nil)
26
+ {name.to_s => @data}.to_yaml
27
+ end
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,30 @@
1
+ module Flex
2
+ class Template
3
+ class Search < Template
4
+
5
+ def initialize(data, vars=nil)
6
+ super('GET', "/<<index>>/<<type>>/_search", data, vars)
7
+ end
8
+
9
+ def to_a(vars={})
10
+ int = interpolate(vars)
11
+ a = [int[:data]]
12
+ a << @instance_vars unless @instance_vars.nil?
13
+ a
14
+ end
15
+
16
+ def to_msearch(vars={})
17
+ int = interpolate(vars, strict=true)
18
+ header = {}
19
+ header[:index] = int[:vars][:index] if int[:vars][:index]
20
+ header[:type] = int[:vars][:type] if int[:vars][:type]
21
+ [:search_type, :preferences, :routing].each do |k|
22
+ header[k] = int[:vars][k] if int[:vars][k] || int[:vars][:params] && int[:vars][:params][k]
23
+ end
24
+ data, encoded = build_data(int, vars)
25
+ "#{MultiJson.encode(header)}\n#{encoded}\n"
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,13 @@
1
+ module Flex
2
+ class Template
3
+ class SlimSearch < Search
4
+
5
+ # removes the fields param (no _source returned)
6
+ # the result.loaded_collection, will load the records from the db
7
+ def self.variables
8
+ super.add(:params => {:fields => ''})
9
+ end
10
+
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,46 @@
1
+ module Flex
2
+ class Template
3
+ class Tags < Array
4
+
5
+ TAG_REGEXP = /<<\s*(\w+)\s*(?:=([^>]*))*>>/
6
+
7
+ def variables
8
+ tag_variables = {}
9
+ each { |t| tag_variables[t.name] = t.default if t.default || t.optional }
10
+ tag_variables
11
+ end
12
+
13
+ def stringify(structure)
14
+ structure.inspect.gsub(/(?:"#{TAG_REGEXP}"|#{TAG_REGEXP})/) do
15
+ match = $&
16
+ match =~ TAG_REGEXP
17
+ t = Tag.new($1, $2)
18
+ push t unless find{|i| i.name == t.name}
19
+ t.stringify(match !~ /^"/)
20
+ end
21
+ end
22
+
23
+ end
24
+
25
+ class Tag
26
+
27
+ RESERVED = [:path, :data, :params, :page, :no_pruning, :raise]
28
+
29
+ attr_reader :optional, :name, :default
30
+
31
+ def initialize(name, default)
32
+ raise SourceError, ":#{name} is a reserved symbol and cannot be used as a tag name" \
33
+ if RESERVED.include?(name)
34
+ @name = name.to_sym
35
+ @optional = !!default
36
+ @default = YAML.load(default) unless default.nil?
37
+ end
38
+
39
+ def stringify(in_string)
40
+ in_string ? "\#{vars[:#{name}]}" : "vars[:#{name}]"
41
+ end
42
+
43
+ end
44
+
45
+ end
46
+ end
@@ -0,0 +1,140 @@
1
+ module Flex
2
+ module UtilityMethods
3
+
4
+ def configuration
5
+ Configuration
6
+ end
7
+ alias_method :config, :configuration
8
+
9
+ # Anonymous search query: please, consider to use named templates for better performances and programming style
10
+ # data can be a JSON string that will be passed as is, or a YAML string (that will be converted into a ruby hash)
11
+ # or a hash. It can contain interpolation tags as usual.
12
+ # You can pass an optional hash of interpolation arguments (or query string :params).
13
+ # See also the Flex::Template::Search documentation
14
+ def search(data, args={})
15
+ Template::Search.new(data).render(args)
16
+ end
17
+
18
+ # like Flex.search, but it will use the Flex::Template::SlimSearch instead
19
+ def slim_search(data, args={})
20
+ Template::SlimSearch.new(data).render(args)
21
+ end
22
+
23
+ %w[HEAD GET PUT POST DELETE].each do |m|
24
+ class_eval <<-ruby, __FILE__, __LINE__
25
+ def #{m}(*args)
26
+ perform '#{m}', *args
27
+ end
28
+ ruby
29
+ end
30
+
31
+ def json2yaml(json)
32
+ YAML.dump(MultiJson.decode(json))
33
+ end
34
+
35
+ def yaml2json(yaml)
36
+ MultiJson.encode(YAML.load(yaml))
37
+ end
38
+
39
+ # Flex.process_bulk accepts a :collection of objects, that can be hashes or Models
40
+ # you can pass also a :action set to 'index' (default) or 'delete'
41
+ # in order to bulk-index or bulk-delete the whole collection
42
+ # you can use Flex.bulk if you have an already formatted bulk data-string
43
+ def process_bulk(args)
44
+ raise ArgumentError, "Array expected as :collection (got #{args[:collection].inspect})" \
45
+ unless args[:collection].is_a?(Array)
46
+
47
+ index = args[:index] || Configuration.variables[:index]
48
+ type = args[:type] || Configuration.variables[:type]
49
+ action = args[:action] || 'index'
50
+
51
+ meta = {}
52
+ [:version, :routing, :percolate, :parent, :timestamp, :ttl].each do |opt|
53
+ meta["_#{opt}"] = args[opt] if args[opt]
54
+ end
55
+ lines = args[:collection].map do |d|
56
+ # skips indexing for objects that return nil as the indexed_json or are not flex_indexable?
57
+ unless action == 'delete'
58
+ next if d.respond_to?(:flex_indexable?) && !d.flex_flex_indexable?
59
+ json = get_json(d) || next
60
+ end
61
+ m = {}
62
+ m['_index'] = get_index(d) || index
63
+ m['_type'] = get_type(d) || type
64
+ m['_id'] = get_id(d) || d # we could pass an array of ids to delete
65
+ parent = get_parent(d)
66
+ m['_parent'] = parent if parent
67
+ routing = get_routing(d)
68
+ m['_routing'] = routing if routing
69
+ line = {action => meta.merge(m)}.to_json
70
+ line << "\n#{json}" unless action == 'delete'
71
+ line
72
+ end.compact
73
+
74
+ bulk(args.merge(:lines => lines.join("\n") + "\n")) if lines.size > 0
75
+ end
76
+
77
+ def import_collection(collection, options={})
78
+ process_bulk( {:collection => collection,
79
+ :action => 'index'}.merge(options) )
80
+
81
+ end
82
+
83
+ def delete_collection(collection, options={})
84
+ process_bulk( {:collection => collection,
85
+ :action => 'delete'}.merge(options) )
86
+ end
87
+
88
+ private
89
+
90
+ def perform(*args)
91
+ Template.new(*args).render
92
+ end
93
+
94
+ def get_index(d)
95
+ d.class.flex.index if d.class.respond_to?(:flex)
96
+ end
97
+
98
+ def get_type(d)
99
+ case
100
+ when d.respond_to?(:flex) then d.flex.type
101
+ when d.respond_to?(:_type) then d._type
102
+ when d.is_a?(Hash) then d.delete(:_type) || d.delete('_type') || d.delete(:type) || d.delete('type')
103
+ when d.respond_to?(:type) then d.type
104
+ end
105
+ end
106
+
107
+ def get_parent(d)
108
+ case
109
+ when d.respond_to?(:flex) && d.flex.parent_instance(false) then d.flex.parent_instance.id
110
+ when d.respond_to?(:_parent) then d._parent
111
+ when d.respond_to?(:parent) then d.parent
112
+ when d.is_a?(Hash) then d.delete(:_parent) || d.delete('_parent') || d.delete(:parent) || d.delete('parent')
113
+ end
114
+ end
115
+
116
+ def get_routing(d)
117
+ case
118
+ when d.respond_to?(:flex) && d.flex.routing(false) then d.flex.routing
119
+ when d.respond_to?(:_routing) then d._routing
120
+ when d.respond_to?(:routing) then d.routing
121
+ when d.is_a?(Hash) then d.delete(:_routing) || d.delete('_routing') || d.delete(:routing) || d.delete('routing')
122
+ end
123
+ end
124
+
125
+ def get_id(d)
126
+ case
127
+ when d.is_a?(Hash) then d.delete(:_id) || d.delete('_id') || d.delete(:id) || d.delete('id')
128
+ when d.respond_to?(:id) then d.id
129
+ end
130
+ end
131
+
132
+ def get_json(d)
133
+ case
134
+ when d.respond_to?(:flex_source) then d.flex_source
135
+ when d.respond_to?(:to_json) then d.to_json
136
+ end
137
+ end
138
+
139
+ end
140
+ end
data/lib/flex/utils.rb ADDED
@@ -0,0 +1,59 @@
1
+ module Flex
2
+ module Utils
3
+ extend self
4
+
5
+ def data_from_source(source)
6
+ return unless source
7
+ data = case source
8
+ when Hash then stringified_hash(source)
9
+ when /^\s*\{.+\}\s*$/m then source
10
+ when String then YAML.load(source)
11
+ else raise ArgumentError, "expected a String or Hash instance (got #{source.inspect})"
12
+ end
13
+ raise ArgumentError, "the source does not decode to a Hash or String (got #{data.inspect})" \
14
+ unless data.is_a?(Hash) || data.is_a?(String)
15
+ data
16
+ end
17
+
18
+ def deep_merge_hashes(h1, *hashes)
19
+ merged = h1.dup
20
+ hashes.each {|h2| merged.replace(deep_merge_hash(merged,h2))}
21
+ merged
22
+ end
23
+
24
+ def erb_process(source)
25
+ ERB.new(File.read(source)).result
26
+ end
27
+
28
+ def group_array_by(ary)
29
+ h = {}
30
+ ary.each do |i|
31
+ k = yield i
32
+ if h.has_key?(k)
33
+ h[k] << i
34
+ else
35
+ h[k] = [i]
36
+ end
37
+ end
38
+ h
39
+ end
40
+
41
+ def stringified_hash(hash)
42
+ h = {}
43
+ hash.each do |k,v|
44
+ h[k.to_s] = v.is_a?(Hash) ? stringified_hash(v) : v
45
+ end
46
+ h
47
+ end
48
+
49
+ private
50
+
51
+ def deep_merge_hash(h1, h2)
52
+ h2 ||= {}
53
+ h1.merge(h2) do |key, oldval, newval|
54
+ oldval.is_a?(Hash) && newval.is_a?(Hash) ? deep_merge_hash(oldval, newval) : newval
55
+ end
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,11 @@
1
+ module Flex
2
+ class Variables < Hash
3
+
4
+ include Structure::Mergeable
5
+
6
+ def initialize(hash={})
7
+ replace hash
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,51 @@
1
+ require 'prompter'
2
+
3
+ class Flex::SetupGenerator < Rails::Generators::Base
4
+
5
+ # more funny than vanilla thor
6
+ include Prompter::Methods
7
+
8
+ source_root File.expand_path('../templates', __FILE__)
9
+
10
+ def self.banner
11
+ "rails generate flex:setup"
12
+ end
13
+
14
+ def ask_base_name
15
+ @class_name = ask('Please, enter a class name for your Search class. Choose a name not defined in your app.',
16
+ :default => 'FlexSearch', :hint => '[<enter>=FlexSearch]')
17
+ @extender_name = "#{@class_name}Extender"
18
+ end
19
+
20
+ def add_config_flex_file
21
+ template 'flex_config.yml', Rails.root.join('config', 'flex.yml')
22
+ end
23
+
24
+ def create_initializer_file
25
+ template 'flex_initializer.rb.erb', Rails.root.join('config', 'initializers', 'flex.rb')
26
+ end
27
+
28
+ def create_flex_dir
29
+ template 'flex_dir/es.rb.erb', Rails.root.join('app', 'flex', "#{@class_name.underscore}.rb")
30
+ template 'flex_dir/es.yml.erb', Rails.root.join('app', 'flex', "#{@class_name.underscore}.yml")
31
+ template 'flex_dir/es_extender.rb.erb', Rails.root.join('app', 'flex', "#{@extender_name.underscore}.rb")
32
+ end
33
+
34
+
35
+ def show_setup_message
36
+ say <<-text, :style => :green
37
+
38
+ Setup done!
39
+
40
+ During prototyping, remember also:
41
+
42
+ 1. each time you add a `Flex::Model` you should add its name to the "config/initializers/flex.rb"
43
+ 2. each time you add/change a flex.parent relation you should reindex your DB(s) with rake `flex:import FORCE=true`
44
+
45
+ The complete documentation is available at https://github.com/ddnexus/flex/wiki
46
+ If you have any problem with Flex, please report the issue at https://github.com/ddnexus/flex/issues.
47
+ text
48
+ end
49
+
50
+ end
51
+