flex 0.1.0

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 (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
+