esse 0.0.2

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. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rubocop.yml +128 -0
  4. data/CHANGELOG.md +0 -0
  5. data/Gemfile +7 -0
  6. data/Gemfile.lock +60 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +50 -0
  9. data/Rakefile +4 -0
  10. data/bin/console +22 -0
  11. data/bin/setup +8 -0
  12. data/esse.gemspec +39 -0
  13. data/exec/esse +9 -0
  14. data/lib/esse.rb +7 -0
  15. data/lib/esse/backend/index.rb +38 -0
  16. data/lib/esse/backend/index/aliases.rb +69 -0
  17. data/lib/esse/backend/index/create.rb +56 -0
  18. data/lib/esse/backend/index/delete.rb +38 -0
  19. data/lib/esse/backend/index/documents.rb +23 -0
  20. data/lib/esse/backend/index/existance.rb +23 -0
  21. data/lib/esse/backend/index/update.rb +19 -0
  22. data/lib/esse/backend/index_type.rb +32 -0
  23. data/lib/esse/backend/index_type/documents.rb +203 -0
  24. data/lib/esse/cli.rb +29 -0
  25. data/lib/esse/cli/base.rb +11 -0
  26. data/lib/esse/cli/generate.rb +51 -0
  27. data/lib/esse/cli/index.rb +14 -0
  28. data/lib/esse/cli/templates/index.rb.erb +59 -0
  29. data/lib/esse/cli/templates/mappings.json +6 -0
  30. data/lib/esse/cli/templates/serializer.rb.erb +14 -0
  31. data/lib/esse/cluster.rb +58 -0
  32. data/lib/esse/config.rb +73 -0
  33. data/lib/esse/core.rb +89 -0
  34. data/lib/esse/index.rb +21 -0
  35. data/lib/esse/index/actions.rb +10 -0
  36. data/lib/esse/index/backend.rb +13 -0
  37. data/lib/esse/index/base.rb +118 -0
  38. data/lib/esse/index/descendants.rb +17 -0
  39. data/lib/esse/index/inheritance.rb +18 -0
  40. data/lib/esse/index/mappings.rb +47 -0
  41. data/lib/esse/index/naming.rb +64 -0
  42. data/lib/esse/index/settings.rb +48 -0
  43. data/lib/esse/index/type.rb +31 -0
  44. data/lib/esse/index_mapping.rb +38 -0
  45. data/lib/esse/index_setting.rb +41 -0
  46. data/lib/esse/index_type.rb +10 -0
  47. data/lib/esse/index_type/actions.rb +11 -0
  48. data/lib/esse/index_type/backend.rb +13 -0
  49. data/lib/esse/index_type/mappings.rb +42 -0
  50. data/lib/esse/index_type/serializer.rb +87 -0
  51. data/lib/esse/primitives.rb +3 -0
  52. data/lib/esse/primitives/hstring.rb +85 -0
  53. data/lib/esse/template_loader.rb +46 -0
  54. data/lib/esse/types/mapping.rb +0 -0
  55. data/lib/esse/version.rb +5 -0
  56. metadata +215 -0
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require 'ostruct'
5
+ require_relative 'base'
6
+
7
+ module Esse
8
+ module CLI
9
+ class Generate < Base
10
+ NAMESPACE_PATTERN_RE = %r{\:|/|\\}i.freeze
11
+
12
+ def self.source_root
13
+ File.dirname(__FILE__)
14
+ end
15
+
16
+ desc 'index NAME *TYPES', 'Creates a new index'
17
+ def index(name, *types)
18
+ ns_path = name.split(NAMESPACE_PATTERN_RE).tap(&:pop)
19
+ @index_name = Hstring.new(name.to_s).modulize.sub(/Index$/, '') + 'Index'
20
+ @types = types.map { |type| Hstring.new(type) }
21
+ @base_class = base_index_class(*ns_path)
22
+
23
+ base_dir = Esse.config.indices_directory.join(*ns_path)
24
+ index_name = Hstring.new(@index_name).demodulize.underscore.to_s
25
+ template(
26
+ 'templates/index.rb.erb',
27
+ base_dir.join("#{index_name}.rb"),
28
+ )
29
+ @types.each do |type|
30
+ @type = Hstring.new(type).underscore
31
+ copy_file(
32
+ 'templates/mappings.json',
33
+ base_dir.join(index_name, 'templates', "#{@type}_mapping.json"),
34
+ )
35
+ template(
36
+ 'templates/serializer.rb.erb',
37
+ base_dir.join(index_name, 'serializers', "#{@type}_serializer.rb"),
38
+ )
39
+ end
40
+ end
41
+
42
+ protected
43
+
44
+ def base_index_class(*ns)
45
+ return 'ApplicationIndex' if Esse.config.indices_directory.join(*ns, 'application_index.rb').exist?
46
+
47
+ 'Esse::Index'
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'thor'
4
+ require_relative 'base'
5
+ module Esse
6
+ module CLI
7
+ class Index < Base
8
+ desc 'create *INDEX_CLASSES', 'Creates a new index'
9
+ def create(*index_classes)
10
+ # Add action here
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= @index_name %> < <%= @base_class %>
4
+ # plugin :active_record
5
+ # plugin :sequel
6
+ <%- @types.each do |type| -%>
7
+
8
+ define_type :<%= type %> do
9
+ # Collection
10
+ # ==========
11
+ #
12
+ # Collection wraps the data into an array of items that should be serialized. The first argument that is
13
+ # yielded must extends Enumerable.
14
+ # Useful for eager loading data from database or any other repository. Below is an example of a rails like
15
+ # application could load using activerecord.
16
+ #
17
+ # collection do |conditions|
18
+ # context = {}
19
+ # <%= type.camelize %>.where(conditions).find_in_batches(batch_size: 5000) do |batch|
20
+ # yield batch, context, ...
21
+ # end
22
+ # end
23
+ #
24
+ #
25
+ # Serializer
26
+ # ==========
27
+ #
28
+ # The serializer can be any class that respond with the `as_json` class method.
29
+ # And the result of its as_json is a Hash.
30
+ #
31
+ # Here is an example of a simple serializer:
32
+ # app/serializers/<%= type %>_serializer.rb
33
+ # class <%= type.camelize %>Serializer
34
+ # def initialize(<%= type %>, _context)
35
+ # @<%= type %> = <%= type %>
36
+ # end
37
+ #
38
+ # def as_json
39
+ # { '_id' => @<%= type %>.id, 'name' => @<%= type %>.name }
40
+ # end
41
+ # end
42
+ #
43
+ # And here you specify your serializer classe.
44
+ # serializer <%= @index_name %>::Serializers::<%= type.camelize %>Serializer
45
+ #
46
+ # You can also serialize the collection entry using a block:
47
+ #
48
+ # serializer(<%= type %>, context = {}) do
49
+ # hash = {
50
+ # name: <%= type %>.name,
51
+ # }
52
+ # # Context is just an example here. But it's useful for eager loading data.
53
+ # # I'll think a better example when implement this idea.
54
+ # hash[:some_attribute] = <%= type %>.some_attribute if context[:include_some_attribute]
55
+ # hash
56
+ # end
57
+ end
58
+ <%- end -%>
59
+ end
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": {
3
+ "type": "string",
4
+ "index": "analyzed"
5
+ }
6
+ }
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= @index_name %>::Serializers::<%= @type.camelize %>Serializer
4
+ def initialize(<%= @type %>, *_other)
5
+ @entity = <%= @type %>
6
+ end
7
+
8
+ def as_json
9
+ {
10
+ id: @entity.id, # This field is required
11
+ name: @entity.name,
12
+ }
13
+ end
14
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esse
4
+ class Cluster
5
+ ATTRIBUTES = %i[index_prefix index_settings client].freeze
6
+
7
+ # The index prefix. For example an index named UsersIndex.
8
+ # With `index_prefix = 'app1'`. Final index/alias is: 'app1_users'
9
+ attr_accessor :index_prefix
10
+
11
+ # This settings will be passed through all indices during the mapping
12
+ attr_accessor :index_settings
13
+
14
+ attr_reader :id
15
+
16
+ def initialize(id:, **options)
17
+ @id = id.to_sym
18
+ @index_settings = {}
19
+ assign(options)
20
+ end
21
+
22
+ def assign(hash)
23
+ return unless hash.is_a?(Hash)
24
+
25
+ hash.each do |key, value|
26
+ method = (ATTRIBUTES & [key.to_s, key.to_sym]).first
27
+ next unless method
28
+
29
+ public_send(:"#{method}=", value)
30
+ end
31
+ end
32
+
33
+ def client
34
+ @client ||= Elasticsearch::Client.new
35
+ end
36
+
37
+ # Define the elasticsearch client connectio
38
+ # @param client [Elasticsearch::Client, Hash] an instance of elasticsearch/api client or an hash
39
+ # with the settings that will be used to initialize Elasticsearch::Client
40
+ def client=(val)
41
+ @client = if val.is_a?(Hash)
42
+ settings = val.each_with_object({}) { |(k,v), r| r[k.to_sym] = v }
43
+ Elasticsearch::Client.new(settings)
44
+ else
45
+ val
46
+ end
47
+ end
48
+
49
+ def inspect
50
+ attrs = ([:id] + ATTRIBUTES - [:client]).map do |method|
51
+ value = public_send(method)
52
+ format('%<k>s=%<v>p', k: method, v: value) if value
53
+ end.compact
54
+ attrs << format('client=%p', @client)
55
+ format('#<Esse::Cluster %<attrs>s>', attrs: attrs.join(' '))
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module Esse
6
+ # Provides all configurations
7
+ #
8
+ # Example
9
+ # Esse.config do |conf|
10
+ # conf.indices_directory = 'app/indices'
11
+ # end
12
+ class Config
13
+ DEFAULT_CLUSTER_ID = :default
14
+ ATTRIBUTES = %i[indices_directory].freeze
15
+
16
+ # The location of the indices. Defaults to the `app/indices`
17
+ attr_reader :indices_directory
18
+
19
+ def initialize
20
+ self.indices_directory = 'app/indices'
21
+ @clusters = {}
22
+ clusters(DEFAULT_CLUSTER_ID) # initialize the :default client
23
+ end
24
+
25
+ def cluster_ids
26
+ @clusters.keys
27
+ end
28
+
29
+ def clusters(key = DEFAULT_CLUSTER_ID, **options)
30
+ return unless key
31
+
32
+ id = key.to_sym
33
+ (@clusters[id] ||= Cluster.new(id: id)).tap do |c|
34
+ c.assign(options) if options
35
+ yield c if block_given?
36
+ end
37
+ end
38
+
39
+ def indices_directory=(value)
40
+ @indices_directory = value.is_a?(Pathname) ? value : Pathname.new(value)
41
+ end
42
+
43
+ def load(arg)
44
+ case arg
45
+ when Hash
46
+ assign(arg)
47
+ when File, Pathname
48
+ # @TODO Load JSON or YAML
49
+ when String
50
+ # @TODO Load JSON or YAML if File.exist?(arg)
51
+ else
52
+ raise ArgumentError, printf('could not load configuration using: %p', val)
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def assign(hash)
59
+ hash.each do |key, value|
60
+ method = (ATTRIBUTES & [key.to_s, key.to_sym]).first
61
+ next unless method
62
+
63
+ public_send("#{method}=", value)
64
+ end
65
+ if (connections = hash['clusters'] || hash[:clusters]).is_a?(Hash)
66
+ connections.each do |key, value|
67
+ clusters(key).assign(value) if value.is_a?(Hash)
68
+ end
69
+ end
70
+ true
71
+ end
72
+ end
73
+ end
data/lib/esse/core.rb ADDED
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'multi_json'
4
+ require 'elasticsearch'
5
+
6
+ module Esse
7
+ @single_threaded = false
8
+ # Mutex used to protect mutable data structures
9
+ @data_mutex = Mutex.new
10
+
11
+ # Block configurations
12
+ # Esse.config do |conf|
13
+ # conf.indices_directory = 'app/indices/directory'
14
+ # conf.clusters(:v1) do |cluster|
15
+ # cluster.index_prefix = 'backend'
16
+ # cluster.client = Elasticsearch::Client.new
17
+ # cluster.index_settings = {
18
+ # number_of_shards: 2,
19
+ # number_of_replicas: 0
20
+ # }
21
+ # end
22
+ # end
23
+ #
24
+ # Inline configurations
25
+ # Esse.config.indices_directory = 'app/indices/directory'
26
+ # Esse.config.clusters(:v1).client = Elasticsearch::Client.new
27
+ def self.config
28
+ @config ||= Config.new
29
+ yield(@config) if block_given?
30
+ @config
31
+ end
32
+
33
+ # Unless in single threaded mode, protects access to any mutable
34
+ # global data structure in Esse.
35
+ # Uses a non-reentrant mutex, so calling code should be careful.
36
+ # In general, this should only be used around the minimal possible code
37
+ # such as Hash#[], Hash#[]=, Hash#delete, Array#<<, and Array#delete.
38
+ def self.synchronize(&block)
39
+ @single_threaded ? yield : @data_mutex.synchronize(&block)
40
+ end
41
+
42
+ # Generates an unique timestamp to be used as a index suffix.
43
+ # Time.now.to_i could also do the job. But I think this format
44
+ # is more readable for humans
45
+ def self.timestamp
46
+ Time.now.strftime('%Y%m%d%H%M%S')
47
+ end
48
+
49
+ # Simple helper used to fetch Hash value using Symbol and String keys.
50
+ #
51
+ # @param [Hash] the JSON document
52
+ # @option [Array] :delete Removes the hash key and return its value
53
+ # @option [Array] :keep Fetch the hash key and return its value
54
+ # @return [Array([Integer, String, nil], Hash)] return the key value and the modified hash
55
+ def self.doc_id!(hash, delete: %w[_id], keep: %w[id])
56
+ return unless hash.is_a?(Hash)
57
+
58
+ id = nil
59
+ modified = nil
60
+ Array(delete).each do |key|
61
+ k = key.to_s if hash.key?(key.to_s)
62
+ k ||= key.to_sym if hash.key?(key.to_sym)
63
+ next unless k
64
+
65
+ modified ||= hash.dup
66
+ id = modified.delete(k)
67
+ break if id
68
+ end
69
+ return [id, modified] if id
70
+
71
+ modified ||= hash
72
+ Array(keep).each do |key|
73
+ id = modified[key.to_s] || modified[key.to_sym]
74
+ break if id
75
+ end
76
+ [id, modified]
77
+ end
78
+
79
+ require_relative 'config'
80
+ require_relative 'cluster'
81
+ require_relative 'primitives'
82
+ require_relative 'index_type'
83
+ require_relative 'index_setting'
84
+ require_relative 'index_mapping'
85
+ require_relative 'template_loader'
86
+ require_relative 'backend/index'
87
+ require_relative 'backend/index_type'
88
+ require_relative 'version'
89
+ end
data/lib/esse/index.rb ADDED
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'core'
4
+
5
+ module Esse
6
+ class Index
7
+ require_relative 'index/base'
8
+ require_relative 'index/inheritance'
9
+ require_relative 'index/actions'
10
+ require_relative 'index/naming'
11
+ require_relative 'index/type'
12
+ require_relative 'index/settings'
13
+ require_relative 'index/mappings'
14
+ require_relative 'index/descendants'
15
+ require_relative 'index/backend'
16
+
17
+ @cluster_id = nil
18
+
19
+ def_Index(::Esse)
20
+ end
21
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esse
4
+ class Index
5
+ module ClassMethods
6
+ end
7
+
8
+ extend ClassMethods
9
+ end
10
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esse
4
+ class Index
5
+ module ClassMethods
6
+ def backend
7
+ Esse::Backend::Index.new(self)
8
+ end
9
+ end
10
+
11
+ extend ClassMethods
12
+ end
13
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Esse
4
+ class Index
5
+ module ClassMethods
6
+ attr_reader :cluster_id
7
+
8
+ # Define a Index method on the given module that calls the Index
9
+ # method on the receiver. This is how the Esse::Index() method is
10
+ # defined, and allows you to define Index() methods on other modules,
11
+ # making it easier to have custom index settings for all indexes under
12
+ # a namespace. Example:
13
+ #
14
+ # module V1
15
+ # EsIndex = Class.new(Esse::Index)
16
+ # EsIndex.def_Index(self)
17
+ #
18
+ # class Bar < EsIndex
19
+ # # Uses :default elasticsearch client connection
20
+ # end
21
+ #
22
+ # class Baz < EsIndex(:v1)
23
+ # # Uses :v1 elasticsearch client connection
24
+ # end
25
+ # end
26
+ def def_Index(index_module) # rubocop:disable Naming/MethodName
27
+ tap do |model|
28
+ index_module.define_singleton_method(:Index) do |source|
29
+ model.Index(source)
30
+ end
31
+ end
32
+ end
33
+
34
+ # Lets you create a Index subclass with its elasticsearch cluster
35
+ #
36
+ # Example:
37
+ # # Using a custom cluster
38
+ # Esse.config.clusters(:v1).client = Elasticsearch::Client.new
39
+ # class UsersIndex < Esse::Index(:v1)
40
+ # end
41
+ #
42
+ # # Using :default cluster
43
+ # class UsersIndex < Esse::Index
44
+ # end
45
+ def Index(source) # rubocop:disable Naming/MethodName
46
+ klass = Class.new(self)
47
+
48
+ valid_ids = Esse.config.cluster_ids
49
+ klass.cluster_id = \
50
+ case source
51
+ when Esse::Cluster
52
+ source.id
53
+ when String, Symbol
54
+ id = source.to_sym
55
+ id if valid_ids.include?(id)
56
+ end
57
+
58
+ msg = <<~MSG
59
+ We could not resolve the index cluster using the argument %<arg>p. \n
60
+ It must be previously defined in the `Esse.config' settings. \n
61
+ Here is the list of cluster ids we have configured: %<ids>s\n
62
+
63
+ You can ignore this cluster id entirely. That way the :default id will be used.\n
64
+ Example: \n
65
+ class UsersIndex < Esse::Index\n
66
+ end\n
67
+ MSG
68
+ unless klass.cluster_id
69
+ raise ArgumentError.new, format(msg, arg: source, ids: valid_ids.map(&:inspect).join(', '))
70
+ end
71
+
72
+ klass.type_hash = {}
73
+ klass
74
+ end
75
+
76
+ # Sets the client_id associated with the Index class.
77
+ # This can be used directly on Esse::Index to set the :default es cluster
78
+ # to be used by subclasses, or to override the es client used for specific indices:
79
+ # Esse::Index.cluster_id = :v1
80
+ # ArtistIndex = Class.new(Esse::Index)
81
+ # ArtistIndex.cluster_id = :v2
82
+ def cluster_id=(cluster_id)
83
+ @cluster_id = cluster_id
84
+ end
85
+
86
+ # @return [Symbol] reads the @cluster_id instance variable or :default
87
+ def cluster_id
88
+ @cluster_id || Config::DEFAULT_CLUSTER_ID
89
+ end
90
+
91
+ # @return [Esse::Cluster] an instance of cluster based on its cluster_id
92
+ def cluster
93
+ unless Esse.config.cluster_ids.include?(cluster_id)
94
+ raise NotImplementedError, <<~MSG
95
+ There is no cluster configured for this index. Use `Esse.config.clusters(cluster_id) { ... }' define the elasticsearch
96
+ client connection.
97
+ MSG
98
+ end
99
+
100
+ Esse.synchronize { Esse.config.clusters(cluster_id) }
101
+ end
102
+
103
+ def inspect
104
+ if self == Index
105
+ super
106
+ elsif abstract_class?
107
+ "#{super}(abstract)"
108
+ elsif index_name?
109
+ "#{super}(Index: #{index_name})"
110
+ else
111
+ "#{super}(Index is not defined)"
112
+ end
113
+ end
114
+ end
115
+
116
+ extend ClassMethods
117
+ end
118
+ end