Functional way to make multiple API requests with Clojure - api

I'm working on a Clojure application that will interact with a web API to return a random result meeting a specific criterion. Because of the limitations of the API, I have to retrieve a number of results and use Clojure to filter out an appropriate one.
I would say 99% or more of the time, at least one of the results from the first request will meet my criterion. But because of that other 1%, I feel I have to build in functionality to make multiple requests. Obviously the odds are pretty good that the second request will be successful but I'm leery about writing a function that recurs endlessly until it gets the right result in case something goes wrong and I end up DDoSing the API.
My first thought was to build a counter into the function and limit it to, say, five requests. But that feels a bit procedural as an approach. What's the idiomatic Clojure way to go about this?
Here's what I have so far:
(ns randchar.api
(:require [clj-http.client :as client]))
(defn api-request
[url]
(get-in
(client/request
{:url url
:method :get
:content-type :json
:as :json}) [:body :results]))
(defn filter-results
"Returns the first result that meets the criterion."
[results criterion]
(take 1
(filter #(= (get-in % [:nested :sub-nested]) criterion) results)))
(defn build-url
"Returns a url with randomized query string for the api request; not shown."
[]
)
(defn return-result
"Currently recurs endlessly if no results meet the criterion."
[criterion]
(let [x (filter-results (api-request (build-url)) criterion)]
(if (not (empty? x))
x
(recur))))

You can try something like:
(defn return-result
[criterion count]
(->> (repeatedly count #(filter-results (api-request build-url) criterion))
(filter (complement empty?))
first))

Related

"Update can not be used in this context"

I am new to clojure and I am working on a problem...
I'm am trying to find the frequency of all the instructors I have in a file, but I have no idea where to start. I did this before in a different program and it worked, but now i am getting an error that reads "Update can not be used in this context"
(defn read-lines [filename]
(with-open [rdr (clojure.java.io/reader filename)]
(doall (line-seq rdr))))
(defn classes [s]
(reduce conj (map hash-map [:semester :title :crn :code :levels :credits
:campus :section :capacity :actual :starthout :startmin
:endhour :endmin :weekday :room :datestart
:dateend :schtypye :instructor :prereq :corereq] (.split s ";"))))
(println(map classes (read-lines "C:/Users/Rohil's Computer/Desktop/textfile.txt")))
(loop [semester->instructor {}
[{:keys [semester instructor] :as row} & rows] classes]
(if (nil? row)
semester->instructor
(recur (update semester->instructor semester (fnil conj []) instructor) rows)))
Since you really didn't ask a specific question I'm going to extrapolate given what you said and the code that you posted.
The real question is "Where do I start?"
In Clojure that answer is always "at the repl"
so first you defined read-lines, which seems to take a file name and returns a collection with each item in the collection being the line of the file passed in
That's simple enough and easy to test at the repl.
but then you have the classes function which seems to take a semi-colon delimited string and tries to split it at the semi colon to make a map of the hard-coded keys to values in the string.
After that it kind of goes off the rails.
So lets pick up before that and look at a simpler example; follow along at the repl
(defn classes [s]
(reduce conj (map hash-map [:instructor :semester]
(.split s ";"))))
(map classes ["bob;1" "jack;2" "bob;3"]) returns a collection of maps. in this case it returns ({:semester "1", :instructor "bob"}
{:semester "2", :instructor "jack"}
{:semester "3", :instructor "bob"})
we want to get the frequencies of all names so lets pull those out into a simple list by mapping the instructor keyword over this collection of maps like so:
(map :instructor (map classes ["bob;1" "jack;2" "bob;3"])) so we get
("bob" "jack" "bob")
now we can just call frequencies on that collection to get a map of name to frequency like this:
(frequencies (map :instructor (map classes ["bob;1" "jack;2" "bob;3"])))
this returns {"jack" 1, "bob" 2}
Now that each of those individual pieces seems to be working at the repl we can put them all together:
(->> (read-lines "C:/Users/Rohil's Computer/Desktop/textfile.txt")
(map classes)
(map :instructor)
frequencies)

How do I get the results of the Plucky Query inside my controller?

I'm missing something simple - I do not want to access the results of this query in a view.
Here is the query:
#adm = Admin.where({:id => {"$ne" => params[:id].to_s},:email => params[:email]})
And of course when you inspect you get:
#adm is #<MongoMapper::Plugins::Querying::DecoratedPluckyQuery:0x007fb4be99acd0>
I understand (from asking the MM guys) why this is the case - they wished to delay the results of the actual query as long as possible, and only get a representation of the query object until we render (in a view!).
But what I'm trying to ascertain in my code is IF one of my params matches or doesn't match the result of my query in the controller so I can either return an error message or proceed.
Normally in a view I'm going to do:
#adm.id
To get the BSON out of this. When you try this on the Decorated Query of course it fails:
NoMethodError (undefined method `id' for #<MongoMapper::Plugins::Querying::DecoratedPluckyQuery:0x007fb4b9e9f118>)
This is because it's not actually a Ruby Object yet, it's still the query proxy.
Now I'm fundamentally missing something because I never read a "getting started with Ruby" guide - I just smashed my way in here and learned through brute-force. So, what method do I call to get the results of the Plucky Query?
The field #adm is set to a query as you've seen. So, to access the results, you'll need to trigger execution of the query. There are a variety of activation methods you can call, including all, first, and last. There's a little documentation here.
In this case, you could do something like:
adm_query = Admin.where({:id => {"$ne" => params[:id].to_s},:email => params[:email]})
#adm_user = adm_query.first
That would return you the first user and after checking for nil
if #adm_user.nil?
# do something if no results were found
end
You could also limit the query results:
adm_query = Admin.where( ... your query ...).limit(1)

More efficient Active Record query for large number of columns

I'm trying to work out a more efficient way to add a note count, with a couple of simple where conditions applied to the query. This can take forever, though, as there are as many as 20K records to iterate over. Would welcome any thinking on this.
def reblog_array(notes)
data = []
notes.select('note_type, count(*) as count').where(:note_type => 'reblog', :created_at => Date.today.years_ago(1)..Date.today).group('DATE(created_at)').each do |n|
data << n.count
end
return data
end
This is what's passed to reblog_array(notes) from my controller.
#tumblr = Tumblr.find(params[:id])
#notes = Note.where("tumblr_id = '#{#tumblr.id}'")
From what I can tell, you are trying to calculate how many reblogs/day this Tumblr account/blog had? If so,
notes.where(:note_type => 'reblog', :created_at => Date.today.years_ago(1)..Date.today).group('DATE(created_at)').count.values
should give you the right result, without having to iterate over the result list again. One thing to note, your call right now won't indicate when there are days with 0 reblogs. If you drop the call to #values, you'll get a hash of date => count.
As an aside and in case you didn't know, I'd also suggest making more use of the ActiveRecord relations:
Class Tumblr
has_many :notes
end
#tumblr = Tumblr.find(params[:id])
#notes = #tumblr.notes
this way you avoid writing code like Note.where("tumblr_id = '#{#tumblr.id}'"). It's best to avoid string-interpolated parameters, in favour of code like Note.where(:tumblr_id => #tumblr.id) or Note.where("tumblr_id = ?", #tumblr.id) to leave less chance that you'll write code vulnerable to SQL injection

How can I test :inclusion validation in Rails using RSpec

I have the following validation in my ActiveRecord.
validates :active, :inclusion => {:in => ['Y', 'N']}
I am using the following to test my model validations.
should_not allow_value('A').for(:active)
should allow_value('Y').for(:active)
should allow_value('N').for(:active)
Is there a cleaner and more through way of testing this? I am currently using RSpec2 and shoulda matchers.
EDIT
After some looking around I only found, this probably an 'ok' way of testing this, shoulda does not provide anything for this and anyone who requires it can write their own custom matcher for it.(And probably contribute it back to the project). Some links to discussions that might be intresting:
Links which indicate to the above . Link 1 , Link 2
should_ensure_value_in_range This one comes close to what can be used, but only accepts ranges and not a list of values. Custom matcher can be based on this.
Use shoulda_matchers
In recent versions of shoulda-matchers (at least as of v2.7.0), you can do:
expect(subject).to validate_inclusion_of(:active).in_array(%w[Y N])
This tests that the array of acceptable values in the validation exactly matches this spec.
In earlier versions, >= v1.4 , shoulda_matchers supports this syntax:
it {should ensure_inclusion_of(:active).in_array(%w[Y N]) }
If you have more elements to test than a boolean Y/N then you could also try.
it "should allow valid values" do
%w(item1 item2 item3 item4).each do |v|
should allow_value(v).for(:field)
end
end
it { should_not allow_value("other").for(:role) }
You can also replace the %w() with a constant you have defined in your model so that it tests that only the constant values are allowed.
CONSTANT = %w[item1 item2 item3 item4]
validates :field, :inclusion => CONSTANT
Then the test:
it "should allow valid values" do
Model::CONSTANT.each do |v|
should allow_value(v).for(:field)
end
end
I found one custom shoulda matcher (in one of the projects I was working on) which attempts to coming close to test something like this:
Examples:
it { should validate_inclusion_check_constraint_on :status, :allowed_values => %w(Open Resolved Closed) }
it { should validate_inclusion_check_constraint_on :age, :allowed_values => 0..100 }
The matcher tries to ensure that there is a DB constraint which blows up when it tries to save it.I will attempt to give the essence of the idea. The matches? implementation does something like:
begin
#allowed_values.each do |value|
#subject.send("#{#attribute}=", value)
#subject.save(:validate => false)
end
rescue ::ActiveRecord::StatementInvalid => e
# Returns false if the exception message contains a string matching the error throw by SQL db
end
I guess if we slightly change the above to say #subject.save and let Rails validation blow up, we can return false when the exception string contains something which close matches the real exception error message.
I know this is far from perfect to contributed back to the project, but I guess might not be a bad idea to add into your project as a custom matcher if you really want to test a lot of the :inclusion validation.

assigning random url to a resource in rails 3

I need to generate a random url to my Topic model(for example) as such:
http://localhost:3000/9ARb123
So how can I do this in rails?
Note: the random string must contain digits, small and capital letters.
Something like this perhaps
#config/routes.rb
match "/:random_id" => "topics#show", :constraints => {:random_id => /([a-zA-Z]|\d){3,6}/}
will match a random string of 3-6 random letters/numbers to the show method of your Topics controller. Make sure to declare other resources above this matcher, as something like "http://localhost:3000/pies" will route to Topics#show instead of Pies#index.
To generate a random url for your Topic you can go something like this:
#app/models/topic.rb
before_create :generate_random_id
def generate_random_id
#generates a random hex string of length 6
random_id = SecureRandom.hex(3)
end
Patricks answer should work - but it only covers routing incoming requests.
If you're still using the standard routes (eg topic_path) to create your links, it will still use the normal routes.
If you run rake routes, you should see the name of the route you created with with the random_id. (You may need to name it using :as => 'random_route')
If you call that instead of the standard topic_path you should get the route you are after