soulheart 0.0.2 → 0.0.4

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: eb5870c5f1cf50c4ebac25363602c2f3cf0c4dd8
4
- data.tar.gz: ab86a41e246856d395eacc5b896f36f2cb0ed254
3
+ metadata.gz: b170565afcbbfc53d9ab5f0a46713af88803a3a4
4
+ data.tar.gz: 58445b7ecdf5713487ba1af3539ad06137a04877
5
5
  SHA512:
6
- metadata.gz: 83fff39abcf059750d36f89a5aae935e2a8cc579cbfa248b0ede8abe29b2d79316ec25f486415132c8570e4a6a05f8a9300d5a8f41eea1517aeaeaede5830d31
7
- data.tar.gz: 5f1ec6654a3b373ea5a4ab686005b85484e4a11d2ffd7b7e00d39932121736475d5d8176801f8f3823500780c14d4283f6c749ab663caed75ebefe0337f59ef1
6
+ metadata.gz: 169f0de49bcd01b4ec5b2182f0075e220ea25b0059f16bffb9347567a1012d2925c6efef9a35b212825ba6989a97e60f18970b47f8d5df2e9bed2ea6dedf7fb6
7
+ data.tar.gz: b5b5de55c31b6cd03be318f05c08780302b3e1eee20b124e4eda1b737ffba4eee526b051d6969643ee7709b9f90123c485b21f9efdcdf2c9677c6fe0e66201be
@@ -1,70 +1,76 @@
1
- Soulheart [![Build Status](https://travis-ci.org/sethherr/soulheart.svg)](https://travis-ci.org/sethherr/soulheart) [![Code Climate](https://codeclimate.com/github/sethherr/soulheart/badges/gpa.svg)](https://codeclimate.com/github/sethherr/soulheart) [![Test Coverage](https://codeclimate.com/github/sethherr/soulheart/badges/coverage.svg)](https://codeclimate.com/github/sethherr/soulheart/coverage)
2
- ========
1
+ # <img src="https://raw.githubusercontent.com/sethherr/soulheart/master/logo.png" alt="Soulheart" width="200"> Soulheart [![Build Status](https://travis-ci.org/sethherr/soulheart.svg)](https://travis-ci.org/sethherr/soulheart) [![Code Climate](https://codeclimate.com/github/sethherr/soulheart/badges/gpa.svg)](https://codeclimate.com/github/sethherr/soulheart) [![Test Coverage](https://codeclimate.com/github/sethherr/soulheart/badges/coverage.svg)](https://codeclimate.com/github/sethherr/soulheart/coverage)
3
2
 
4
- This is an updated fork of [Seatgeek/Soulmate](https://github.com/seatgeek/soulmate) to address a few issues - namely [CORS support](../../issues/2), [minimum entry length](../../issues/3) and [playing better with Selectize & Select2](../../issues/4) - also the future.
5
3
 
6
- Since [Seatgeek no longer uses Soulmate](https://news.ycombinator.com/item?id=9317891), and this is a pretty large rewrite that doesn't emphasize backward compatibility, it's a new project and gem.
4
+ **Soulheart is a ready to use remote data source for autocomplete**. It supports:
7
5
 
8
- ========
6
+ - pagination
7
+ - categories
8
+ - sorting by priority (not just alphabetically)
9
+ - arbitrary returns
10
+ - loading data via gists
11
+ - mounting standalone or inside of a rails app
9
12
 
10
- **Every item has to have a unique name**
13
+ ... and is [instantly deployable to heroku](https://heroku.com/deploy) (for free).
11
14
 
12
- Loading data:
15
+ To get started, check out examples and documentation at [sethherr.github.io/soulheart](https://sethherr.github.io/soulheart).
13
16
 
14
- Pushing new options via rake task. Use a gist url! Do it with .csvs!
15
17
 
16
- Structuring data
18
+ ---
17
19
 
18
- The only required attribute is term, which is the search term to go by. So this would be a valid thing:
20
+ ### Adding data
19
21
 
20
- ```javascript
21
- [
22
- { "term": "Something sweet" },
23
- { "term": "The color blue" }
24
- ]
25
- ```
22
+ You can add data from json, CSV and TSV files.
26
23
 
27
- Here are all the possible (non-required) values:
24
+ Adding data is very simple - all you need is a `text` value.
28
25
 
26
+ Soulheart uses [line delineated JSON streams](https://en.wikipedia.org/wiki/JSON_Streaming#Line_delimited_JSON), so it doesn't have to load the whole file into memory. Which just means - put each object onto a seperate line.
29
27
 
30
- | Key | Type | Default |
31
- | ----------- | ----- | ---------- |
32
- | `priority` | integer | 100 |
33
- | `types` | string | 'default' |
34
- | `data` | hash | {} |
28
+ For the simplest case, with just text values in JSON:
35
29
 
36
- e.g.
30
+ { "text": "Jamis" }
31
+ { "text": "Specialized" }
32
+ { "text": "Trek" }
37
33
 
38
- ```javascript
39
- [
40
- {
41
- "term": "Something sweet",
42
- "priority": 99,
43
- "types": "niceness", // A set type for searching by type
44
- "data": { // Any key value pair you want, this is the return value
45
- "id": 99,
46
- "url": "http://example.com",
47
- "whatever": "The color purple"
48
- }
49
- },
50
- { "term": "The color blue" }
51
- ]
52
- ```
34
+ It accepts local files:
35
+
36
+ soulheart load my_json_file.json
37
+
38
+ or remote files:
39
+
40
+ soulheart load https://gist.githubusercontent.com/sethherr/96dbc011e508330ceec4/raw/95122b1fc9de85f241cd048f32b94568f54134e0/manufacturers.tsv
41
+
42
+
43
+ In addition to term, there are a few optional values -
53
44
 
54
- *If you set `term` in `data`, it will respond with that rather than the term it searches by. I haven't figured out a use case for this yet, but I'm sure one exists.*
45
+ | Key | Default | What it does |
46
+ | ------------ | ----------- | ------------ |
47
+ | `priority` | `100` | Higher numbers come first |
48
+ | `category` | `'default'` | Sets the category |
49
+ | `data` | `{}` | Returned object from search - the text and category will be added to this if you don't specify them. |
50
+
51
+ Here is an example of what a possible hash you could pass is
52
+
53
+ { "text": "Jamis", "category": "Bike Manufacturer" }
54
+ { "text": "Specialized" }
55
+ { "text": "Trek" }
56
+
57
+ *If you set `text` in `data`, it will respond with that rather than the term it searches by. I haven't figured out a use case for this yet, but I'm sure one exists.*
55
58
 
56
59
  ======
57
60
 
58
- I'm testing with:
61
+ I'm testing with: `ruby >= 2.1` and `redis >= 3`.
59
62
 
60
- - ruby >= 2.1
61
- - redis >= 3
63
+ Run `bundle exec guard` to run the specs while you work, it will just test the files you change.
62
64
 
63
- To test, run `bundle exec guard`. It will live reload the files you change.
65
+ This repo includes a `config.ru` and a `Gemfile.lock` so it (and any forks of it) can be deployed to heroku. They shouldn't be in the Gem itself.
64
66
 
65
67
 
66
68
  ======
67
69
 
70
+ This is an updated fork of [Seatgeek/Soulmate](https://github.com/seatgeek/soulmate) to address a few issues - namely [CORS support](../../issues/2), [minimum entry length](../../issues/3) and [playing better with Selectize & Select2](../../issues/4) - also the future.
71
+
72
+ Since [Seatgeek no longer uses Soulmate](https://news.ycombinator.com/item?id=9317891), and this isn't backward compatible it's a new project and gem.
73
+
68
74
  :x::o::x::o::x::o: *Soulmate's README follows ([issue for making new documentation](../../issues/1))*
69
75
 
70
76
  Soulmate is a tool to help solve the common problem of developing a fast autocomplete feature. It uses Redis's sorted sets to build an index of partially completed words and the corresponding top matching items, and provides a simple sinatra app to query them. Soulmate finishes your sentences.
data/Rakefile CHANGED
@@ -1,29 +1,29 @@
1
1
  #!/usr/bin/env rake
2
2
 
3
- require "bundler/gem_helper"
3
+ require 'bundler/gem_helper'
4
4
 
5
5
  begin
6
6
  Bundler.setup(:default, :development)
7
7
  rescue Bundler::BundlerError => e
8
8
  $stderr.puts e.message
9
- $stderr.puts "Run `bundle install` to install missing gems"
9
+ $stderr.puts 'Run `bundle install` to install missing gems'
10
10
  exit e.status_code
11
11
  end
12
12
  require 'rake'
13
13
 
14
- REDIS_DIR = File.expand_path(File.join("..", "test"), __FILE__)
15
- REDIS_CNF = File.join(REDIS_DIR, "test.conf")
16
- REDIS_PID = File.join(REDIS_DIR, "db", "redis.pid")
14
+ REDIS_DIR = File.expand_path(File.join('..', 'test'), __FILE__)
15
+ REDIS_CNF = File.join(REDIS_DIR, 'test.conf')
16
+ REDIS_PID = File.join(REDIS_DIR, 'db', 'redis.pid')
17
17
  REDIS_LOCATION = ENV['REDIS_LOCATION']
18
18
 
19
- desc "Run rcov and manage server start/stop"
20
- task :rcoverage => [:start, :rcov, :stop]
19
+ desc 'Run rcov and manage server start/stop'
20
+ task rcoverage: [:start, :rcov, :stop]
21
21
 
22
- desc "Start the Redis server"
22
+ desc 'Start the Redis server'
23
23
  task :start do
24
24
  redis_running = \
25
25
  begin
26
- File.exists?(REDIS_PID) && Process.kill(0, File.read(REDIS_PID).to_i)
26
+ File.exist?(REDIS_PID) && Process.kill(0, File.read(REDIS_PID).to_i)
27
27
  rescue Errno::ESRCH
28
28
  FileUtils.rm REDIS_PID
29
29
  false
@@ -36,21 +36,20 @@ task :start do
36
36
  end
37
37
  end
38
38
 
39
- desc "Stop the Redis server"
39
+ desc 'Stop the Redis server'
40
40
  task :stop do
41
- if File.exists?(REDIS_PID)
42
- Process.kill "INT", File.read(REDIS_PID).to_i
41
+ if File.exist?(REDIS_PID)
42
+ Process.kill 'INT', File.read(REDIS_PID).to_i
43
43
  FileUtils.rm REDIS_PID
44
44
  end
45
45
  end
46
46
 
47
+ require 'rspec/core/rake_task'
47
48
 
48
- require "rspec/core/rake_task"
49
-
50
- desc "Run all specs"
49
+ desc 'Run all specs'
51
50
  RSpec::Core::RakeTask.new(:spec) do |t|
52
51
  t.rspec_opts = %w(--color)
53
52
  t.verbose = false
54
53
  end
55
54
 
56
- task :default => :spec
55
+ task default: :spec
@@ -12,95 +12,45 @@ require 'optparse'
12
12
  require 'tempfile'
13
13
 
14
14
  parser = OptionParser.new do |opts|
15
- opts.banner = "Usage: soulheart [options] COMMAND"
15
+ opts.banner = 'Usage: soulheart [options] COMMAND'
16
16
 
17
- opts.separator ""
18
- opts.separator "Options:"
17
+ opts.separator ''
18
+ opts.separator 'Options:'
19
19
 
20
- opts.on("-r", "--redis [HOST:PORT]", "Redis connection string") do |host|
20
+ opts.on('-r', '--redis [HOST:PORT]', 'Redis connection string') do |host|
21
21
  Soulheart.redis = host
22
22
  end
23
23
 
24
- opts.on("-s", "--stop-words [FILE]", "Path to file containing a list of stop words") do |fn|
24
+ opts.on('-s', '--stop-words [FILE]', 'Path to file containing a list of stop words') do |fn|
25
25
  File.open(fn) do |file|
26
- Soulheart.stop_words = file.readlines.map{ |l| l.strip }.reject{ |w| w.empty? }
26
+ Soulheart.stop_words = file.readlines.map(&:strip).reject(&:empty?)
27
27
  end
28
28
  end
29
29
 
30
- opts.on("-h", "--help", "Show this message") do
30
+ opts.on('-h', '--help', 'Show this message') do
31
31
  puts opts
32
32
  exit
33
33
  end
34
34
 
35
- opts.on("-b", "--batch-size", "Number of lines to read at a time") do |size|
35
+ opts.on('-b', '--batch-size', 'Number of lines to read at a time') do |size|
36
36
  BATCH_SIZE = size
37
37
  end
38
38
 
39
- opts.separator ""
40
- opts.separator "Commands:"
41
- opts.separator " load TYPE FILE Replaces collection specified by TYPE with items read from FILE in the JSON lines format."
42
- opts.separator " add TYPE Adds items to collection specified by TYPE read from stdin in the JSON lines format."
39
+ opts.separator ''
40
+ opts.separator 'Commands:'
41
+ opts.separator ' load TYPE FILE Replaces collection specified by TYPE with items read from FILE in the JSON lines format.'
42
+ opts.separator ' add TYPE Adds items to collection specified by TYPE read from stdin in the JSON lines format.'
43
43
  opts.separator " remove TYPE Removes items from collection specified by TYPE read from stdin in the JSON lines format. Items only require an 'id', all other fields are ignored."
44
- opts.separator " query TYPE QUERY Queries for items from collection specified by TYPE."
44
+ opts.separator ' query TYPE QUERY Queries for items from collection specified by TYPE.'
45
45
  end
46
46
 
47
- def generate(type, file)
48
- include Soulheart::Helpers
49
-
50
- begin
51
- temp = Tempfile.new("soulheart")
52
-
53
- if File.exists?(file)
54
- start_time = Time.now.to_i
55
- base = "soulheart-index:#{type}"
56
- database = "soulheart-data:#{type}"
57
- # hset = "*4\r\n$4\r\nHSET\r\n$#{database.length}\r\n#{database}\r\n$"
58
- # del = "*2\r\n$3\r\nDEL\r\n$"
59
- begin
60
- f = File.open(file)
61
- # cleanup
62
- phrases = Soulheart.redis.smembers(base)
63
- phrases.each do |phrase|
64
- temp << gen_redis_proto("DEL", phrase)
65
- # temp << del + phrase.length.to_s + "\r\n" + phrase + "\r\n"
66
- end
67
- temp << gen_redis_proto("DEL", base)
68
- # temp << del + base.length.to_s + "\r\n" + base + "\r\n"
69
- while !f.eof?
70
- line = f.gets.chomp
71
- line =~ /"id":(\d+)/
72
- id = $1
73
- line =~ /"score":(\d+)/
74
- score = $1
75
- json = MultiJson.decode(line)
76
- temp << gen_redis_proto("HSET", database, id, line)
77
- # temp << hset + $1.length.to_s + "\r\n" + $1 + "\r\n$" + line.length.to_s + "\r\n" + line + "\r\n"
78
- phrase = json.key?("aliases") ? json["term"] + " " + json["aliases"] : json["term"]
79
- prefixes_for_phrase(phrase).each do |p|
80
- temp << gen_redis_proto("SADD", base, p)
81
- temp << gen_redis_proto("ZADD", base + ":" + p, score, id)
82
- end
83
- end
84
- ensure
85
- f.close
86
- end
87
- puts "Converted in #{Time.now.to_i - start_time} second(s)"
88
- puts "Importing into redis ..."
89
- `time redis-cli --pipe < #{temp.path}`
90
- else
91
- puts "Couldn't open file: #{file}"
92
- end
93
- ensure
94
- temp.close
95
- end
96
- end
97
47
 
98
48
  def load(file)
99
49
  require 'uri'
100
- if file =~ URI::regexp
50
+ if file =~ URI.regexp
101
51
  require 'open-uri'
102
52
  f = open(file)
103
- elsif File.exists?(file)
53
+ elsif File.exist?(file)
104
54
  f = File.open(file)
105
55
  else
106
56
  puts "Couldn't open file: #{file}"
@@ -113,17 +63,17 @@ def load(file)
113
63
  lines = []
114
64
  begin
115
65
  if file.match(/(c|t)sv\z/i)
116
- puts "Reading a CSV"
66
+ puts 'Reading a CSV'
117
67
  require 'csv'
118
- sep = file.match(/tsv\z/i) ? "\t" : ","
68
+ sep = file.match(/tsv\z/i) ? "\t" : ','
119
69
  CSV.foreach(f, headers: true, col_sep: sep) do |row|
120
70
  lines << row.to_hash
121
71
  count += 1
122
72
  end
123
73
  elsif file.match(/json\z/i)
124
- puts "Reading JSON"
74
+ puts 'Reading JSON'
125
75
  puts "Loading items in batches of #{BATCH_SIZE} ..."
126
- while !f.eof?
76
+ until f.eof?
127
77
  lines = []
128
78
  BATCH_SIZE.times do
129
79
  break if f.eof?
@@ -132,7 +82,7 @@ def load(file)
132
82
  end
133
83
  end
134
84
  else
135
- puts "unknown File type"
85
+ puts 'unknown File type'
136
86
  end
137
87
  ensure
138
88
  f.close
@@ -164,7 +114,7 @@ end
164
114
  def query(type, query)
165
115
  puts "> Querying '#{type}' for '#{query}'"
166
116
  matcher = Soulheart::Matcher.new(type)
167
- results = matcher.matches_for_term(query, :limit => 0)
117
+ results = matcher.matches_for_term(query, limit: 0)
168
118
  results.each do |item|
169
119
  puts MultiJson.encode(item)
170
120
  end
@@ -172,10 +122,10 @@ def query(type, query)
172
122
  end
173
123
 
174
124
  def gen_redis_proto(*cmd)
175
- proto = "*"+cmd.length.to_s+"\r\n"
125
+ proto = '*' + cmd.length.to_s + "\r\n"
176
126
  cmd.each{|arg|
177
- proto << "$"+arg.bytesize.to_s+"\r\n"
178
- proto << arg+"\r\n"
127
+ proto << '$' + arg.bytesize.to_s + "\r\n"
128
+ proto << arg + "\r\n"
179
129
  }
180
130
  proto
181
131
  end
@@ -6,23 +6,17 @@ begin
6
6
  rescue LoadError
7
7
  require 'rubygems'
8
8
  require 'vegas'
9
- end
9
+ end
10
10
  require 'soulheart/server'
11
11
 
12
-
13
- Vegas::Runner.new(Soulheart::Server, 'soulheart-web', {
14
- :before_run => lambda {|v|
15
- # path = (ENV['RESQUECONFIG'] || v.args.first)
16
- # load path.to_s.strip if path
17
- }
18
- }) do |runner, opts, app|
19
- opts.on("-r", "--redis [HOST:PORT]", "Redis connection string") do |host|
12
+ Vegas::Runner.new(Soulheart::Server, 'soulheart-web') do |runner, opts|
13
+ opts.on('-r', '--redis [HOST:PORT]', 'Redis connection string') do |host|
20
14
  runner.logger.info "Using Redis connection string '#{host}'"
21
15
  Soulheart.redis = host
22
16
  end
23
- opts.on("-s", "--stop-words [FILE]", "Path to file containing a list of stop words") do |fn|
17
+ opts.on('-s', '--stop-words [FILE]', 'Path to file containing a list of stop words') do |fn|
24
18
  File.open(fn) do |file|
25
- Soulheart.stop_words = file.readlines.map{ |l| l.strip }.reject{ |w| w.empty? }
19
+ Soulheart.stop_words = file.readlines.map(&:strip).reject(&:empty?)
26
20
  end
27
21
  end
28
22
  end
@@ -1,9 +1,7 @@
1
1
  module Soulheart
2
-
3
2
  class Base
4
-
5
3
  include Helpers
6
-
4
+
7
5
  attr_accessor :type
8
6
 
9
7
  def redis
@@ -13,15 +11,15 @@ module Soulheart
13
11
  def cache_length
14
12
  10 * 60 # Setting to 10 minutes, but making it possible to edit down the line
15
13
  end
16
-
14
+
17
15
  def base_id
18
- ENV['RACK_ENV'] != 'test' ? "soulheart:" : "soulheart_test:"
16
+ ENV['RACK_ENV'] != 'test' ? 'soulheart:' : 'soulheart_test:'
19
17
  end
20
18
 
21
19
  def set_category_combos_array
22
20
  redis.expire category_combos_id, 0
23
- ar = redis.smembers(categories_id).map{ |c| normalize(c) }.uniq.sort
24
- ar = 1.upto(ar.size).flat_map {|n| ar.combination(n).map{|el| el.join('')}}
21
+ ar = redis.smembers(categories_id).map { |c| normalize(c) }.uniq.sort
22
+ ar = 1.upto(ar.size).flat_map { |n| ar.combination(n).map { |el| el.join('') } }
25
23
  ar.last.replace('all')
26
24
  redis.sadd category_combos_id, ar
27
25
  ar
@@ -31,15 +29,19 @@ module Soulheart
31
29
  "#{base_id}category_combos:"
32
30
  end
33
31
 
32
+ def category_combos
33
+ redis.smembers(category_combos_id)
34
+ end
35
+
34
36
  def categories_id
35
37
  "#{base_id}categories:"
36
38
  end
37
39
 
38
- def category_id(name='all')
40
+ def category_id(name = 'all')
39
41
  "#{categories_id}#{name}:"
40
42
  end
41
43
 
42
- def no_query_id(category=category_id)
44
+ def no_query_id(category = category_id)
43
45
  "all:#{category}"
44
46
  end
45
47
 
@@ -47,8 +49,8 @@ module Soulheart
47
49
  "#{base_id}database:"
48
50
  end
49
51
 
50
- def cache_id(type='all')
52
+ def cache_id(type = 'all')
51
53
  "#{base_id}cache:#{type}:"
52
54
  end
53
55
  end
54
- end
56
+ end
@@ -3,7 +3,7 @@ require 'redis'
3
3
 
4
4
  module Soulheart
5
5
  module Config
6
- DEFAULT_STOP_WORDS = ["vs", "at", "the"]
6
+ DEFAULT_STOP_WORDS = %w(vs at the)
7
7
 
8
8
  # Accepts:
9
9
  # 1. A Redis URL String 'redis://host:port/db'
@@ -23,14 +23,12 @@ module Soulheart
23
23
  # create a new one.
24
24
  def redis
25
25
  @redis ||= (
26
- url = URI(@redis_url || ENV["REDIS_URL"] || "redis://127.0.0.1:6379/0")
27
- ::Redis.new({
28
- # driver: :hiredis,
26
+ url = URI(@redis_url || ENV['REDIS_URL'] || 'redis://127.0.0.1:6379/0')
27
+ ::Redis.new( # driver: :hiredis,
29
28
  host: url.host,
30
29
  port: url.port,
31
30
  db: url.path[1..-1],
32
- password: url.password
33
- })
31
+ password: url.password)
34
32
  )
35
33
  end
36
34
 
@@ -2,21 +2,17 @@
2
2
 
3
3
  module Soulheart
4
4
  module Helpers
5
- def blank?
6
- respond_to?(:empty?) ? !!empty? : !self
7
- end
8
-
9
5
  def normalize(str)
10
6
  # Letter, Mark, Number, Connector_Punctuation (Chinese, Japanese, etc.)
11
7
  str.downcase.gsub(/[^\p{Word}\ ]/i, '').strip
12
8
  end
13
-
9
+
14
10
  def prefixes_for_phrase(phrase)
15
11
  words = normalize(phrase).split(' ').reject do |w|
16
12
  Soulheart.stop_words.include?(w)
17
13
  end
18
14
  words.map do |w|
19
- (0..(w.length-1)).map{ |l| w[0..l] }
15
+ (0..(w.length - 1)).map { |l| w[0..l] }
20
16
  end.flatten.uniq
21
17
  end
22
18
  end
@@ -1,7 +1,5 @@
1
1
  module Soulheart
2
-
3
2
  class Loader < Base
4
-
5
3
  def default_items_hash(text, category)
6
4
  category ||= 'default'
7
5
  {
@@ -12,7 +10,7 @@ module Soulheart
12
10
  'data' => {
13
11
  'text' => text,
14
12
  'category' => category
15
- },
13
+ }
16
14
  }
17
15
  end
18
16
 
@@ -57,31 +55,36 @@ module Soulheart
57
55
  end
58
56
 
59
57
  def clean(item)
60
- raise ArgumentError, "Items must have text" unless item["text"]
61
- default_items_hash(item.delete('text'), item.delete('category')).
62
- tap{ |i| i['data'].merge!(item.delete('data')) if item['data'] }.
63
- tap{ |i| i['priority'] = item.delete('priority').to_f if item['priority'] }.
64
- merge item
58
+ fail ArgumentError, 'Items must have text' unless item['text']
59
+ default_items_hash(item.delete('text'), item.delete('category'))
60
+ .tap { |i| i['data'].merge!(item.delete('data')) if item['data'] }
61
+ .tap { |i| i['priority'] = item.delete('priority').to_f if item['priority'] }
62
+ .merge item
65
63
  end
66
64
 
67
- def add_item(item, category_base_id=nil, cleaned: false)
65
+ def add_item(item, category_base_id = nil, cleaned: false)
68
66
  unless cleaned
69
67
  item = clean(item)
70
68
  category_base_id ||= category_id(item['category'])
69
+ item.keys.select{ |k| k[/data-/i] }.each do |key|
70
+ item['data'].merge!({
71
+ "#{key.gsub(/data-/i,'')}" => item.delete(key)
72
+ })
73
+ end
71
74
  unless redis.smembers(categories_id).include?(item['category'])
72
75
  redis.sadd categories_id, item['category']
73
76
  end
74
77
  end
75
78
  redis.pipelined do
76
- redis.zadd(no_query_id(category_base_id), item["priority"], item["term"]) # Add to master set for queryless searches
79
+ redis.zadd(no_query_id(category_base_id), item['priority'], item['term']) # Add to master set for queryless searches
77
80
  # store the raw data in a separate key to reduce memory usage, if it's cleaned it's done
78
81
  redis.hset(results_hashes_id, item['term'], MultiJson.encode(item['data'])) unless cleaned
79
- phrase = ([item["term"]] + (item["aliases"] || [])).join(' ')
82
+ phrase = ([item['term']] + (item['aliases'] || [])).join(' ')
80
83
  # Store all the prefixes
81
84
  prefixes_for_phrase(phrase).each do |p|
82
85
  redis.sadd(base_id, p) unless cleaned # remember prefix in a master set
83
86
  # store the normalized term in the index for each of the categories
84
- redis.zadd("#{category_base_id}#{p}", item["priority"], item["term"])
87
+ redis.zadd("#{category_base_id}#{p}", item['priority'], item['term'])
85
88
  end
86
89
  end
87
90
  item
@@ -89,19 +92,19 @@ module Soulheart
89
92
 
90
93
  # remove only cares about an item's id, but for consistency takes an object
91
94
  def remove(item)
92
- prev_item = Soulheart.redis.hget(base_id, item["term"])
95
+ prev_item = Soulheart.redis.hget(base_id, item['term'])
93
96
  if prev_item
94
97
  prev_item = MultiJson.decode(prev_item)
95
98
  # undo the operations done in add
96
99
  Soulheart.redis.pipelined do
97
- Soulheart.redis.hdel(base_id, prev_item["term"])
98
- phrase = ([prev_item["term"]] + (prev_item["aliases"] || [])).join(' ')
100
+ Soulheart.redis.hdel(base_id, prev_item['term'])
101
+ phrase = ([prev_item['term']] + (prev_item['aliases'] || [])).join(' ')
99
102
  prefixes_for_phrase(phrase).each do |p|
100
103
  Soulheart.redis.srem(base_id, p)
101
- Soulheart.redis.zrem("#{base_id}:#{p}", prev_item["term"])
104
+ Soulheart.redis.zrem("#{base_id}:#{p}", prev_item['term'])
102
105
  end
103
106
  end
104
107
  end
105
108
  end
106
109
  end
107
- end
110
+ end
@@ -1,28 +1,29 @@
1
1
  module Soulheart
2
-
3
2
  class Matcher < Base
4
- def initialize(params={})
3
+ def initialize(params = {})
5
4
  @opts = self.class.default_params_hash.merge params
6
5
  clean_opts
7
6
  end
8
7
 
8
+ attr_accessor :opts
9
+
9
10
  def self.default_params_hash
10
11
  {
11
12
  'page' => 1,
12
13
  'per_page' => 5,
13
14
  'categories' => [],
14
- 'query' => '',
15
+ 'q' => '', # Query
15
16
  'cache' => true
16
17
  }
17
18
  end
18
19
 
19
20
  def clean_opts
20
21
  unless @opts['categories'] == '' || @opts['categories'] == []
21
- @opts['categories'] = @opts['categories'].split(/,|\+/) unless @opts['categories'].kind_of?(Array)
22
- @opts['categories'] = @opts['categories'].map{ |s| normalize(s) }.uniq.sort
22
+ @opts['categories'] = @opts['categories'].split(/,|\+/) unless @opts['categories'].is_a?(Array)
23
+ @opts['categories'] = @opts['categories'].map { |s| normalize(s) }.uniq.sort
23
24
  @opts['categories'] = [] if @opts['categories'].length == redis.scard(categories_id)
24
25
  end
25
- @opts['query'] = normalize(@opts['query']).split(' ')
26
+ @opts['q'] = normalize(@opts['q']).split(' ') unless @opts['q'].is_a?(Array)
26
27
  # .reject{ |i| i && i.length > 0 } .split(' ').reject{ Soulmate.stop_words.include?(w) }
27
28
  @opts
28
29
  end
@@ -36,12 +37,12 @@ module Soulheart
36
37
  end
37
38
 
38
39
  def cache_id_from_opts
39
- "#{cache_id(categories_string)}#{@opts['query'].join(':')}"
40
+ "#{cache_id(categories_string)}#{@opts['q'].join(':')}"
40
41
  end
41
42
 
42
43
  def interkeys_from_opts(cid)
43
44
  # If there isn't a query, we use a special key in redis
44
- @opts['query'].empty? ? [no_query_id(cid)] : @opts['query'].map { |w| "#{cid}#{w}" }
45
+ @opts['q'].empty? ? [no_query_id(cid)] : @opts['q'].map { |w| "#{cid}#{w}" }
45
46
  end
46
47
 
47
48
  def matches
@@ -55,17 +56,16 @@ module Soulheart
55
56
  end
56
57
  offset = (@opts['page'].to_i - 1) * @opts['per_page'].to_i
57
58
  limit = @opts['per_page'].to_i + offset - 1
58
-
59
+
59
60
  limit = 0 if limit < 0
60
61
  ids = redis.zrevrange(cachekey, offset, limit) # Using 'ids', even though keys are now terms
61
62
  if ids.size > 0
62
63
  results = redis.hmget(results_hashes_id, *ids)
63
- results = results.reject{ |r| r.nil? } # handle cached results for ids which have since been deleted
64
+ results = results.reject(&:nil?) # handle cached results for ids which have since been deleted
64
65
  results.map { |r| MultiJson.decode(r) }
65
66
  else
66
67
  []
67
- end
68
+ end
68
69
  end
69
-
70
70
  end
71
- end
71
+ end
@@ -1,33 +1,30 @@
1
1
  require 'sinatra/base'
2
2
  require 'soulheart'
3
- require 'rack/contrib'
4
3
 
5
4
  module Soulheart
6
-
7
5
  class Server < Sinatra::Base
8
6
  include Helpers
9
-
7
+
10
8
  before do
11
- content_type 'application/json', :charset => 'utf-8'
9
+ content_type 'application/json', charset: 'utf-8'
12
10
  headers['Access-Control-Allow-Origin'] = '*'
13
11
  headers['Access-Control-Allow-Methods'] = 'POST, PUT, GET, OPTIONS'
14
12
  headers['Access-Control-Request-Method'] = '*'
15
13
  headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Authorization'
16
14
  end
17
-
15
+
18
16
  get '/' do
19
17
  matches = Matcher.new(params).matches
20
- MultiJson.encode({ matches: matches })
18
+ MultiJson.encode(matches: matches)
21
19
  end
22
20
 
23
- get '/status' do
24
- MultiJson.encode({ soulheart: Soulheart::VERSION, :status => "ok" })
21
+ get '/status' do
22
+ MultiJson.encode(soulheart: Soulheart::VERSION, status: 'ok')
25
23
  end
26
-
24
+
27
25
  not_found do
28
- content_type 'application/json', :charset => 'utf-8'
29
- MultiJson.encode({ :error => "not found" })
26
+ content_type 'application/json', charset: 'utf-8'
27
+ MultiJson.encode(error: 'not found')
30
28
  end
31
-
32
29
  end
33
30
  end
@@ -1,3 +1,3 @@
1
1
  module Soulheart
2
- VERSION = "0.0.2"
3
- end
2
+ VERSION = '0.0.4'
3
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: soulheart
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Seth Herr
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-06-17 00:00:00.000000000 Z
11
+ date: 2015-06-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hiredis
@@ -80,20 +80,6 @@ dependencies:
80
80
  - - ">="
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
- - !ruby/object:Gem::Dependency
84
- name: rack-contrib
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
83
  - !ruby/object:Gem::Dependency
98
84
  name: rake
99
85
  requirement: !ruby/object:Gem::Requirement
@@ -145,13 +131,8 @@ executables:
145
131
  extensions: []
146
132
  extra_rdoc_files: []
147
133
  files:
148
- - ".gitignore"
149
- - ".rspec"
150
- - ".travis.yml"
151
- - Gemfile
152
- - Guardfile
153
134
  - LICENSE.md
154
- - README.markdown
135
+ - README.md
155
136
  - Rakefile
156
137
  - bin/soulheart
157
138
  - bin/soulheart-web
@@ -163,14 +144,6 @@ files:
163
144
  - lib/soulheart/matcher.rb
164
145
  - lib/soulheart/server.rb
165
146
  - lib/soulheart/version.rb
166
- - logo.png
167
- - soulheart.gemspec
168
- - spec/fixtures/multiple_categories.json
169
- - spec/soulheart/loader_spec.rb
170
- - spec/soulheart/matcher_spec.rb
171
- - spec/soulheart/server_spec.rb
172
- - spec/soulheart_spec.rb
173
- - spec/spec_helper.rb
174
147
  homepage: https://github.com/sethherr/soulheart
175
148
  licenses:
176
149
  - MIT
data/.gitignore DELETED
@@ -1,21 +0,0 @@
1
- # rdoc generated
2
- rdoc
3
-
4
- # yard generated
5
- doc
6
- .yardoc
7
-
8
- # bundler
9
- .bundle
10
-
11
- gem
12
-
13
- test/db/*.rdb
14
-
15
- dump.rdb
16
- tmp/
17
-
18
- Gemfile.lock
19
-
20
- .rvmrc
21
- .ruby-*
data/.rspec DELETED
@@ -1,2 +0,0 @@
1
- --color
2
- --format documentation
@@ -1,12 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.1.0
4
- bundler_args: "--without=guard"
5
- notifications:
6
- disabled: true
7
- script:
8
- - bundle exec rake spec
9
- # - bundle exec rubocop
10
- addons:
11
- code_climate:
12
- repo_token: e72d8fa152922cef05c311cd49d3db9016b82486b712399a8e7c7da2af5e071e
data/Gemfile DELETED
@@ -1,22 +0,0 @@
1
- source 'http://rubygems.org'
2
-
3
- group :development do
4
- gem 'rspec', '~> 2.14.1'
5
- gem 'bundler'
6
- gem 'guard'
7
- gem 'guard-rspec', '~> 4.2.8'
8
- gem 'rack-contrib'
9
- gem 'rubocop'
10
- end
11
-
12
- group :test do
13
- gem 'rack-test'
14
- gem "codeclimate-test-reporter", require: nil
15
- end
16
-
17
- gem 'rake'
18
- gem 'redis', '>= 3.0.1'
19
- gem 'hiredis', '~> 0.4.5'
20
- gem 'vegas', '>= 0.1.0'
21
- gem 'sinatra'
22
- gem 'multi_json', '>= 1.11.0'
data/Guardfile DELETED
@@ -1,9 +0,0 @@
1
- rspec_opts = {
2
- failed_mode: :focus
3
- }
4
-
5
- guard :rspec, cmd: "bundle exec rspec" do
6
- watch(%r{^spec/.+_spec\.rb$})
7
- watch(%r{^lib/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
8
- watch(%r{^lib/soulheart/(.+)\.rb$}) { |m| "spec/soulheart/#{m[1]}_spec.rb" }
9
- end
data/logo.png DELETED
Binary file
@@ -1,24 +0,0 @@
1
- # -*- encoding: utf-8 -*-
2
- require File.expand_path('../lib/soulheart/version', __FILE__)
3
-
4
- Gem::Specification.new do |gem|
5
- gem.version = Soulheart::VERSION
6
- gem.authors = ["Seth Herr"]
7
- gem.email = ["seth.william.herr@gmail.com"]
8
- gem.description = gem.summary = "Simple, fast autocomplete server for Ruby and Rails"
9
- gem.homepage = "https://github.com/sethherr/soulheart"
10
- gem.license = "MIT"
11
- gem.executables = ["soulheart", "soulheart-web"]
12
- gem.files = `git ls-files | grep -Ev '^(test)'`.split("\n")
13
- gem.name = "soulheart"
14
- gem.require_paths = ["lib"]
15
- gem.add_dependency 'hiredis', '~> 0.4.5'
16
- gem.add_dependency 'redis', '>= 3.0.6'
17
- gem.add_dependency 'vegas', '>= 0.1.0'
18
- gem.add_dependency 'json'
19
- gem.add_dependency 'sinatra'
20
- gem.add_development_dependency 'rack-contrib'
21
- gem.add_development_dependency 'rake'
22
- gem.add_development_dependency 'rspec'
23
- gem.add_development_dependency 'rubocop'
24
- end
@@ -1,15 +0,0 @@
1
- {"text":"Steel","category":"Frame Materials" }
2
- {"text":"Brompton Bicycle","priority":51,"category":"Frame Manufacturer","data":{"id":8,"url":"http://www.brompton.com"}}
3
- {"text":"Jamis","priority":75,"category":"Frame Manufacturer","data":{"id":2222,"url":"http://jamisbikes.com"}}
4
- {"text":"Surly","priority":102,"category":"Frame Manufacturer"}
5
- {"text":"Jagwire","priority":40,"category":"Manufacturer"}
6
- {"text":"Jannd","priority":41,"category":"Manufacturer"}
7
- {"text":"Sram","priority":50,"category":"Manufacturer","data":{"id":8,"url":"http://sram.com"}}
8
- {"text":"Brooks England LTD.","priority":50,"category":"Manufacturer","data":{"id":200,"url":"http://www.brooksengland.com/"}}
9
- {"text":"Dodger Stadium","priority":84,"data":{"id":1,"url":"\/dodger-stadium-tickets\/","subtitle":"Los Angeles, CA"},"aliases":["Chavez Ravine"]}
10
- {"text":"Angel Stadium","priority":90,"data":{"id":28,"url":"\/angel-stadium-tickets\/","subtitle":"Anaheim, CA"},"aliases":["Edison International Field of Anaheim"]}
11
- {"text":"Chase Field ","priority":80,"data":{"id":30,"url":"\/chase-field-tickets\/","subtitle":"Phoenix, AZ"},"aliases":["Bank One Ballpark", "Bank One Stadium"]}
12
- {"text":"Sun Life Stadium","priority":75,"data":{"id":29,"url":"\/sun-life-stadium-tickets\/","subtitle":"Miami, FL"},"aliases":["Dolphins Stadium","Land Shark Stadium"]}
13
- {"text":"Turner Field","priority":50,"data":{"id":2,"url":"\/turner-field-tickets\/","subtitle":"Atlanta, GA"}}
14
- {"text":"Citi Field","priority":92,"data":{"id":3,"url":"\/citi-field-tickets\/","subtitle":"Atlanta, GA"},"aliases":["Shea Stadium"]}
15
- {"text":"中国佛山 李小龙","priority":94,"data":{"id":8,"url":"\/Bruce Lee\/","subtitle":"Chinese Foshan"},"aliases":["Li XiaoLong"]}
@@ -1,96 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe Soulheart::Loader do
4
-
5
- describe :clean_data do
6
- it "sets the default category, priority and normalizes term" do
7
- item = { 'text' => ' FooBar' }
8
- result = Soulheart::Loader.new.clean(item)
9
- expect(result['priority']).to eq(100)
10
- expect(result['term']).to eq('foobar')
11
- expect(result['category']).to eq('default')
12
- expect(result['data']['text']).to eq(' FooBar')
13
- end
14
-
15
- it "doesn't overwrite the submitted params" do
16
- item = {
17
- 'text' => 'Cool ',
18
- 'priority' => '50',
19
- 'category' => 'Gooble',
20
- 'data' => {
21
- 'id' => 199
22
- }
23
- }
24
- result = Soulheart::Loader.new.clean(item)
25
- expect(result['term']).to eq('cool')
26
- expect(result['priority']).to eq(50)
27
- expect(result['data']['text']).to eq('Cool ')
28
- expect(result['data']['id']).to eq(199)
29
- expect(result['category']).to eq('gooble')
30
- expect(result['data']['category']).to eq('Gooble')
31
- end
32
-
33
- it "raises argument error if text is passed" do
34
- expect{
35
- Soulheart::Loader.new.clean({'name' => 'stuff'})
36
- }.to raise_error(/must have/i)
37
- end
38
- end
39
-
40
- describe :add_item do
41
- it "adds an item, adds prefix scopes, adds category" do
42
- item = {
43
- 'text' => 'Brompton Bicycle',
44
- 'priority' => 50,
45
- 'category' => 'Gooble',
46
- 'data' => {
47
- 'id' => 199
48
- }
49
- }
50
- loader = Soulheart::Loader.new
51
- redis = loader.redis
52
- redis.expire loader.results_hashes_id, 0
53
- loader.add_item(item)
54
- redis = loader.redis
55
- target = "{\"text\":\"Brompton Bicycle\",\"category\":\"Gooble\",\"id\":199}"
56
- result = redis.hget(loader.results_hashes_id, 'brompton bicycle')
57
- expect(result).to eq(target)
58
- prefixed = redis.zrevrange "#{loader.category_id('gooble')}:brom", 0, -1
59
- expect(prefixed[0]).to eq('brompton bicycle')
60
- expect(redis.smembers(loader.categories_id).include?('gooble')).to be_true
61
- end
62
- end
63
-
64
- describe :store_terms do
65
- it "stores terms by priority and adds categories for each possible category combination" do
66
- items = []
67
- file = File.read('spec/fixtures/multiple_categories.json')
68
- file.each_line { |l| items << MultiJson.decode(l) }
69
- loader = Soulheart::Loader.new
70
- redis = loader.redis
71
- loader.delete_categories
72
- loader.load(items)
73
-
74
- cat_prefixed = redis.zrevrange "#{loader.category_id('frame manufacturermanufacturer')}:brom", 0, -1
75
- expect(cat_prefixed.count).to eq(1)
76
- expect(redis.smembers(loader.categories_id).count).to be > 3
77
-
78
- prefixed = redis.zrevrange "#{loader.category_id('all')}:bro", 0, -1
79
- expect(prefixed.count).to eq(2)
80
- expect(prefixed[0]).to eq('brompton bicycle')
81
- end
82
-
83
- it "stores terms by priority and doesn't add run categories if none are present" do
84
- items = [
85
- {'text' => 'cool thing', 'category' => 'AWESOME'},
86
- {'text' => 'Sweet', 'category' => ' awesome'}
87
- ]
88
- loader = Soulheart::Loader.new
89
- redis = loader.redis
90
- loader.delete_categories
91
- loader.load(items)
92
- expect(redis.smembers(loader.category_combos_id).count).to eq(1)
93
- end
94
- end
95
-
96
- end
@@ -1,104 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe Soulheart::Matcher do
4
-
5
- describe :clean_opts do
6
- it "Has the keys we need" do
7
- target_keys = ['categories', 'query', 'page', 'per_page']
8
- keys = Soulheart::Matcher.default_params_hash.keys
9
- expect((target_keys - keys).count).to eq(0)
10
- end
11
-
12
- it "obeys stop words"
13
- end
14
-
15
- describe :category_id_from_opts do
16
- it 'gets the id for one' do
17
- Soulheart::Loader.new.reset_categories(['cool', 'test'])
18
- matcher = Soulheart::Matcher.new('categories' => ['some_category'])
19
- expect(matcher.category_id_from_opts).to eq(matcher.category_id('some_category'))
20
- end
21
-
22
- it 'gets the id for all of them' do
23
- Soulheart::Loader.new.reset_categories(['cool', 'test', 'boo'])
24
- matcher = Soulheart::Matcher.new('categories' => 'cool, boo, test')
25
- expect(matcher.category_id_from_opts).to eq(matcher.category_id('all'))
26
- end
27
- end
28
-
29
- describe :categories_string do
30
- it 'does all if none' do
31
- Soulheart::Loader.new.reset_categories(['cool', 'test'])
32
- matcher = Soulheart::Matcher.new('categories' => '')
33
- expect(matcher.categories_string).to eq('all')
34
- end
35
- it "correctly concats a string of categories" do
36
- Soulheart::Loader.new.reset_categories(['cool', 'some_category', 'another cat', 'z9', 'stuff'])
37
- matcher = Soulheart::Matcher.new({'categories' => 'some_category, another cat, z9'})
38
- expect(matcher.categories_string).to eq('another catsome_categoryz9')
39
- end
40
- end
41
-
42
-
43
- describe :matches do
44
- it "With no params, gets all the matches, ordered by priority and name" do
45
- store_terms_fixture
46
- opts = { 'per_page' => 100, 'cache' => false }
47
- matches = Soulheart::Matcher.new(opts).matches
48
- expect(matches.count).to be > 10
49
- expect(matches[0]['text']).to eq('Surly')
50
- end
51
-
52
- it "With no query but with categories, matches categories" do
53
- store_terms_fixture
54
- opts = { 'per_page' => 100, 'cache' => false, 'categories' => 'manufacturer' }
55
- matches = Soulheart::Matcher.new(opts).matches
56
- expect(matches.count).to eq(4)
57
- expect(matches[0]['text']).to eq('Sram')
58
- end
59
-
60
- it "Gets the matches matching query and priority for one item in query, all categories" do
61
- store_terms_fixture
62
- opts = { 'per_page' => 100, 'query' => 'j', 'cache' => false }
63
- matches = Soulheart::Matcher.new(opts).matches
64
- expect(matches.count).to eq(3)
65
- expect(matches[0]['text']).to eq('Jamis')
66
- end
67
-
68
- it "Gets the matches matching query and priority for one item in query, one category" do
69
- store_terms_fixture
70
- opts = { 'per_page' => 100, 'query' => 'j', 'cache' => false, 'categories' => 'manufacturer' }
71
- matches = Soulheart::Matcher.new(opts).matches
72
- expect(matches.count).to eq(2)
73
- expect(matches[0]['text']).to eq('Jannd')
74
- end
75
-
76
- it "Gets pages and uses them" do
77
- # Pagination wrecked my mind, hence the multitude of tests
78
- items = [
79
- {"text" => 'First item', 'priority' => '11000' },
80
- {"text" => 'Second item', 'priority' => '1999' },
81
- {"text" => 'Third item', 'priority' => 1900 },
82
- {"text" => 'Fourth item', 'priority' => 1800 },
83
- {"text" => 'Fifth item', 'priority' => 1750 },
84
- {"text" => 'Sixth item', 'priority' => 1700 },
85
- {"text" => 'Seventh item', 'priority' => 1699 }
86
- ]
87
- loader = Soulheart::Loader.new
88
- loader.delete_categories
89
- loader.load(items)
90
-
91
- page1 = Soulheart::Matcher.new({'per_page' => 1, 'cache' => false}).matches
92
- expect(page1[0]['text']).to eq('First item')
93
-
94
- page2 = Soulheart::Matcher.new({'per_page' => 1, 'page' => 2, 'cache' => false}).matches
95
- expect(page2.count).to eq(1)
96
- expect(page2[0]['text']).to eq('Second item')
97
-
98
- page3 = Soulheart::Matcher.new({'per_page' => 2, 'page' => 3, 'cache' => false}).matches
99
- expect(page3[0]['text']).to eq('Fifth item')
100
- expect(page3[1]['text']).to eq('Sixth item')
101
- end
102
- end
103
-
104
- end
@@ -1,26 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe Soulheart::Server do
4
-
5
- describe :search do
6
- it "Has CORS headers, JSON Content-Type and it succeeds" do
7
- get '/'
8
- expect(last_response.headers['Access-Control-Allow-Origin']).to eq('*')
9
- expect(last_response.headers['Access-Control-Request-Method']).to eq('*')
10
- expect(last_response.headers['Content-Type']).to match('json')
11
- expect(last_response.status).to eq(200)
12
- end
13
- end
14
-
15
- describe :status do
16
- it "Has cors headers and is valid JSON" do
17
- get '/status'
18
- expect(last_response.headers['Access-Control-Allow-Origin']).to eq('*')
19
- expect(last_response.headers['Access-Control-Request-Method']).to eq('*')
20
- expect(last_response.headers['Content-Type']).to match('json')
21
- expect(JSON.parse(last_response.body)['soulheart']).to match(/\d/)
22
- end
23
- end
24
-
25
-
26
- end
@@ -1,29 +0,0 @@
1
- require 'spec_helper'
2
-
3
- describe Soulheart do
4
-
5
- it 'has a version number' do
6
- expect(Soulheart::VERSION).not_to be nil
7
- end
8
-
9
- it "has a test base_id" do
10
- expect(Soulheart::Base.new.base_id).to eq('soulheart_test:')
11
- end
12
-
13
- it "collates (? probably not the word I'm looking for) all the things" do
14
- base = Soulheart::Base.new
15
- base.redis.expire base.categories_id, 0
16
- base.redis.sadd base.categories_id, ['George', 'category one', 'other thing ']
17
- result = base.set_category_combos_array
18
- expect(result.include?("category one")).to be_true
19
- expect(result.include?("george")).to be_true
20
- expect(result.include?("other thing")).to be_true
21
- expect(result.include?("georgeother thing")).to be_true
22
- expect(result.include?("category oneother thing")).to be_true
23
- expect(result.include?("category onegeorge")).to be_true
24
- expect(result.include?("georgecategory one")).to be_false
25
- expect(result.include?("all")).to be_true
26
- expect(result.include?("category onegeorgeother thing")).to be_false
27
- end
28
-
29
- end
@@ -1,31 +0,0 @@
1
- if ENV['CODECLIMATE_REPO_TOKEN']
2
- require "codeclimate-test-reporter"
3
- CodeClimate::TestReporter.start
4
- end
5
- require 'rack/test'
6
- require 'rspec'
7
-
8
- $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
9
- require 'soulheart'
10
- require 'soulheart/server'
11
-
12
- ENV['RACK_ENV'] = 'test'
13
-
14
- module RSpecMixin
15
- include Rack::Test::Methods
16
- def app() Soulheart::Server end
17
- end
18
-
19
- RSpec.configure do |config|
20
- config.treat_symbols_as_metadata_keys_with_true_values = true
21
- config.include RSpecMixin
22
- end
23
-
24
- def store_terms_fixture
25
- items = []
26
- file = File.read('spec/fixtures/multiple_categories.json')
27
- file.each_line { |l| items << MultiJson.decode(l) }
28
- loader = Soulheart::Loader.new
29
- loader.delete_categories
30
- loader.load(items)
31
- end