flex 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.md +20 -0
- data/VERSION +1 -0
- data/flex.gemspec +43 -0
- data/lib/flex.rb +418 -0
- data/lib/flex/api_methods.yml +108 -0
- data/lib/flex/class_proxy.rb +12 -0
- data/lib/flex/configuration.rb +57 -0
- data/lib/flex/errors.rb +42 -0
- data/lib/flex/http_clients/patron.rb +27 -0
- data/lib/flex/http_clients/rest_client.rb +38 -0
- data/lib/flex/loader.rb +116 -0
- data/lib/flex/logger.rb +16 -0
- data/lib/flex/model.rb +24 -0
- data/lib/flex/model/class_proxy.rb +45 -0
- data/lib/flex/model/instance_proxy.rb +101 -0
- data/lib/flex/model/manager.rb +67 -0
- data/lib/flex/rails.rb +12 -0
- data/lib/flex/rails/engine.rb +23 -0
- data/lib/flex/rails/helper.rb +16 -0
- data/lib/flex/related_model.rb +16 -0
- data/lib/flex/related_model/class_proxy.rb +23 -0
- data/lib/flex/related_model/class_sync.rb +23 -0
- data/lib/flex/related_model/instance_proxy.rb +28 -0
- data/lib/flex/result.rb +18 -0
- data/lib/flex/result/bulk.rb +20 -0
- data/lib/flex/result/collection.rb +51 -0
- data/lib/flex/result/document.rb +38 -0
- data/lib/flex/result/indifferent_access.rb +11 -0
- data/lib/flex/result/search.rb +51 -0
- data/lib/flex/result/source_document.rb +63 -0
- data/lib/flex/result/source_search.rb +32 -0
- data/lib/flex/structure/indifferent_access.rb +44 -0
- data/lib/flex/structure/mergeable.rb +21 -0
- data/lib/flex/tasks.rb +141 -0
- data/lib/flex/template.rb +187 -0
- data/lib/flex/template/base.rb +29 -0
- data/lib/flex/template/info.rb +50 -0
- data/lib/flex/template/partial.rb +31 -0
- data/lib/flex/template/search.rb +30 -0
- data/lib/flex/template/slim_search.rb +13 -0
- data/lib/flex/template/tags.rb +46 -0
- data/lib/flex/utility_methods.rb +140 -0
- data/lib/flex/utils.rb +59 -0
- data/lib/flex/variables.rb +11 -0
- data/lib/generators/flex/setup/setup_generator.rb +51 -0
- data/lib/generators/flex/setup/templates/flex_config.yml +16 -0
- data/lib/generators/flex/setup/templates/flex_dir/es.rb.erb +18 -0
- data/lib/generators/flex/setup/templates/flex_dir/es.yml.erb +19 -0
- data/lib/generators/flex/setup/templates/flex_dir/es_extender.rb.erb +17 -0
- data/lib/generators/flex/setup/templates/flex_initializer.rb.erb +44 -0
- data/lib/tasks/index.rake +23 -0
- data/test/flex.irt +143 -0
- data/test/flex/configuration.irt +53 -0
- data/test/irt_helper.rb +12 -0
- 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,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
|
+
|