elixir dynamic module call - dynamic

How can I call function func() in a module called App.Reporting.Name
based on the string "name" which is not known until runtime
using String.to_atom or to_existing_atom does not work :
alias App.Reporting.Name
module = "name" |> String.capitalise |> String.to_atom
apply(module, :func, [])
Without the alias, this does not work either
module = "App.Reporting.Name" |> String.to_atom
apply(module, :func, [])
I get an (UndefinedFunctionError) and (module :"App.Reporting.Name" is not available)
thanks

Your second approach is almost correct, you just need to prefix Elixir. because App.Reporting.Name is equal to :"Elixir.App.Reporting.Name", not :"App.Reporting.Name" since Elixir prefixes all module names (names starting with an uppercase letter) with Elixir. before turning it into an atom:
iex(1)> App.Reporting.Name == :"App.Reporting.Name"
false
iex(2)> App.Reporting.Name == :"Elixir.App.Reporting.Name"
true
So, this code should work:
module = "Elixir.App.Reporting.Name" |> String.to_atom
apply(module, :func, [])
and so should this:
module = Module.concat(App.Reporting, "name" |> String.capitalize |> String.to_atom)
apply(module, :func, [])

The reason yours isn't working is because the String.to_atom does just that, turns a string into an atom. Because there is no module called "App.Reporting.Name" it's most likely App.Reporting.Name it errors.
Not sure if this is the best way to do this, just one that sprang to mind. But you could do something like this:
iex(2)> module = "Casing"
"Casing"
iex(3)> Module.concat(String, "#{module}") |> apply(:upcase, ["test sentence"])
"TEST SENTENCE"
Another solution could be to create a macro that automatically does this process, however that is not something I am that great at so you will have to go through the docs here for that one.

Related

Elixir - Manipulating a 2 dimensional list

Hope everybody is having a beautiful 2019 even though we're just a day in.
I am currently working on a small Phoenix app where I'm manipulating PDF files (in the context of this question I'm splitting them) and then uploading them to S3. Later on I have to delete the temporary files created by pdftk ( a pdf tool ) I use to split them up and also show the s3 links in the response body since this is an API request.
The way I have structured this is as following:
Inside my Split module where the core business logic is:
filenames = []
s3_links = []
Enum.map(pages, fn(item) ->
split_filename = item
|> split(filename)
link = split_filename
|> FileHelper.result_file_bytes()
|> ManageS3.upload()
|> FileHelper.save_file(work_group_id, pass)
[filenames ++ split_filename, s3_links ++ link]
end)
|> transform()
{filenames, s3_links}
The important things are split_filename and link
This is what I'm getting when I call an IO.inspect in the transform() method:
[
["87cdcd73-5b27-4757-a472-78aaf6cc6864.pdf",
"Some_S3_LINK00"],
["0ab460ca-5019-4864-b0ff-343966c7d72a.pdf",
"Some_S3_LINK01"]
]
The structuring is [[filename, s3_link], [filename, s3_link]] whereas the desired outcome would be that of [ [list of all filenames], [list of s3 links].
If anybody can lend a hand I would be super grateful. Thanks in advance!
Sidenotes:
Assigning filenames = []; s3_links = [] in the very beginning makes zero sense. Enum.map already maps the input. What you need is probably Enum.reduce/3.
Don’t use the pipe |> operator when the pipe consists of the only call, it is considered an anti-pattern by Elixir core team.
Always start pipes with a term.
Solution:
Reduce the input into the result using Enum.reduce/3 directly to what you need.
pages
|> Enum.reduce([[], []], fn item, [files, links] ->
split_filename = split(item, filename)
link =
split_filename
|> FileHelper.result_file_bytes()
|> ManageS3.upload()
|> FileHelper.save_file(work_group_id, pass)
[[split_filename | files], [link | links]]
end)
|> Enum.map(&Enum.reverse/1)
|> IO.inspect(label: "Before transform")
|> transform()
You did not provide the input to test it, but I believe it should work.
Instead of working on lists of lists, you may want to consider using tuples with lists. Something like the following should work for you.
List.foldl(pages, {[], []}, fn(item, {filenames, links}) ->
filename = split(item, filename)
link =
file_name
|> FileHelper.result_file_bytes()
|> ManagerS3.upload()
|> FileHelper.save_file(work_group_id, pass)
{[filename | filenames], [link | links]}
end)
This will return a value that looks like
{
["87cdcd73-5b27-4757-a472-78aaf6cc6864.pdf",
"0ab460ca-5019-4864-b0ff-343966c7d72a.pdf"],
["Some_S3_LINK00",
"Some_S3_LINK01"]
}
Though, depending on how you are using these values, maybe a list of tuples would be more appropriate. Something like
Enum.map(pages, fn(item) ->
filename = split(item, filename)
link =
filename
|> FileHelper.result_file_bytes()
|> ManageS3.upload()
|> FileHelper.save_file(work_group_id, pass)
{filename, link}
end)
would return
[
{"87cdcd73-5b27-4757-a472-78aaf6cc6864.pdf", "Some_S3_LINK00"},
{"0ab460ca-5019-4864-b0ff-343966c7d72a.pdf", "Some_S3_LINK01"}
]

Elixir - testing a full script

I'm writing a test to check a function (called automatically by GenServer when a new file enters a folder) that calls other functions in the same module with pipes in order to read a file, process its content to insert it if needed and returns a list (:errors and :ok maps).
results looks like :
[
error: "Data not found",
ok: %MyModule{
field1: field1data,
field2: field2data
},
ok: %MyModule{
field1: field1data,
field2: field2data
},
error: "Data not found"
the code :
def processFile(file) do
insertResultsMap =
File.read!(file)
|> getLines()
|> extractMainData()
|> Enum.map(fn(x) -> insertLines(x) end)
|> Enum.group_by(fn x -> elem(x, 0) end)
handleErrors(Map.get(insertResultsMap, :error))
updateAnotherTableWithLines(Map.get(insertResultsMap, :ok))
end
defp getLines(docContent) do
String.split(docContent, "\n")
end
defp extractMainData(docLines) do
Enum.map(fn(x) -> String.split(x, ",") end)
end
defp insertLines([field1, field2, field3, field4]) do
Attrs = %{
field1: String.trim(field1),
field2: String.trim(field2),
field3: String.trim(field3),
field4: String.trim(field4)
}
mymodule.create_stuff(Attrs)
end
defp handleErrors(errors) do
{:ok, file} = File.open(#errorsFile, [:append])
saveErrors(file, errors)
File.close(file)
end
defp saveErrors(_, []), do: :ok
defp saveErrors(file, [{:error, changeset}|rest]) do
changes = for {key, value} <- changeset.changes do
"#{key} #{value}"
end
errors = for {key, {message, _}} <- changeset.errors do
"#{key} #{message}"
end
errorData = "data: #{Enum.join(changes, ", ")} \nErrors: #{Enum.join(errors, ", ")}\n\n"
IO.binwrite(file, errorData)
saveErrors(file, rest)
end
defp updateAnotherTableWithLines(insertedLines) do
Enum.map(insertedLines, fn {:ok, x} -> updateOtherTable(x) end)
end
defp updateOtherTable(dataForUpdate) do
"CLOSE" -> otherModule.doStuff(dataForUpdate.field1, dataForUpdate.field2)
end
I have several questions, and some will be pretty basic since I'm still learning :
What do you think of the code ? Any advices ? (take into account I voluntarily obfuscated names).
If I want to test this, is it the right way to test only processFile function ? Or should I make public more of them and test them individually ?
When I test the processFile function, I check that I'm receiving a list. Any way to make sure this list has only elements I'm waiting for, thus error: "String" or ok: %{}" ?
What do you think of the code? Any advices? (take into account I voluntarily obfuscated names).
Opinion based.
If I want to test this, is it the right way to test only processFile function?
Yes.
Or should I make public more of them and test them individually?
No, this is an implementation detail and testing it is an anti-pattern.
When I test the processFile function, I check that I'm receiving a list. Any way to make sure this list has only elements I'm waiting for, thus error: "String" or ok: %{}"?
You receive a Keyword. To check the explicit value, one might use:
foo = processFile(file)
assert not is_nil(foo[:ok])
OTOH, I’d better return a map from there and pattern match it:
assert %{ok: _} = processFile(file)
To assert that the result does not have anything save for :oks and :errors, one might use list subtraction:
assert Enum.uniq(Keyword.keys(result)) -- [:ok, :error] == []

Substitutions not occurring with Typesafe Config

When I use Config.withValue to create an updated config, substitutions are not re-evaluated, even with a call to resolve:
application.conf:
zooKeeperAddr = "localhost:2181"
zooKeeperAddr2 = ${zooKeeperAddr}
Application code:
val config = ConfigFactory.load()
.withValue("zooKeeperAddr", ConfigValueFactory.fromAnyRef("abc"))
.resolve;
val zooKeeperAddr = config.getAnyRef("zooKeeperAddr")
val zooKeeperAddr2 = config.getAnyRef("zooKeeperAddr2")
println(s"zooKeeperAddr, zooKeeperAddr2 is $zooKeeperAddr, $zooKeeperAddr2")
I expect, of course, to see
zooKeeperAddr, zooKeeperAddr2 is abc, abc
But what I see instead is:
zooKeeperAddr, zooKeeperAddr2 is abc, localhost:2181
How can I get substitutions re-evalauted?
(The larger issue is, I'm trying to inject command-line arguments, in particular, Twitter Module Flags, into the Typesafe Config. Perhaps there's a better way to achieve this goal?
My actual code is:
val config = flag.getAll(false).foldLeft(ConfigFactory.load()){
case (conf, f) if f.isDefined => conf.withValue(f.name, ConfigValueFactory.fromAnyRef(f.get.get))
case (conf, _) => conf
}.resolve
)
So I (the OP) ended up doing the following:
val config = flag.getAll(false).foldLeft(ConfigFactory.empty()){
case (conf, f) if f.isDefined => conf.withValue(f.name, ConfigValueFactory.fromAnyRef(f.get.get))
case (conf, _) => conf
}
.withFallback(ConfigFactory.defaultOverrides())
.withFallback(ConfigFactory.defaultApplication())
.withFallback(ConfigFactory.defaultReference())
.resolve
flag.getAll returns an Iterable[com.twitter.app.Flag]; for each flag that isDefined, we add it to an initaily empty config (ConfigFactory.empty()).
Then we withFallback to, in order, the default overrides (the settings properties), the application config (application.conf, and the default reference (which should include, I hope, all reference.confs in all jars).
withFallback, according to its documentation, "Returns a new value computed by merging this value with another, with keys in this value "winning" over the other one."
Finally, we resolve.
This seems to propagate the substitutions as I want, but I can't help but think the Config API provides an easier way to do this.

Getting type signatures for a function in elm

I'm using elm 0.18.
Let's say I have a function that strings together a bunch of stuff that I threw together in a hurry. It works, but I'm not sure what it's type signature is, and I'd like elm to tell me (or hint for me) that type signature.
For example, I use graphql and have a function that takes a graphql string, a decoder (which also doesn't have a type signature), and a Cmd Msg, and runs it through HttpBuilder.
graphQLPost graphiql decoder msg =
HttpBuilder.post (url ++ "api")
|> HttpBuilder.withStringBody "text/plain" graphiql
|> HttpBuilder.withExpect (Http.expectJson decoder)
|> HttpBuilder.send msg
This works, though I don't know why. I tried fitting it with the type signature graphQLPost : String -> Json.Decode.Decoder -> Cmd Msg, but I get an error.
Figuring out this type signature is not as important to me as finding a way to induce them through elm. Is there a command that I can enter into elm-repl or something that will tell me the signature?
Elm REPL will do this for you:
> import Http
> import HttpBuilder
> type Msg = Msg
> url = "..."
"..." : String
> graphQLPost graphiql decoder msg = \
| HttpBuilder.post (url ++ "api") \
| |> HttpBuilder.withStringBody "text/plain" graphiql \
| |> HttpBuilder.withExpect (Http.expectJson decoder) \
| |> HttpBuilder.send msg
<function>
: String
-> Json.Decode.Decoder a
-> (Result.Result Http.Error a -> msg)
-> Platform.Cmd.Cmd msg
When you write a function and hit <Enter>, it shows you the signature. In this case the signature is:
graphQLPost : String
-> Json.Decode.Decoder a
-> (Result.Result Http.Error a -> msg)
-> Platform.Cmd.Cmd msg
Running elm-make with the --warn option will cause the compiler to suggest that you include a type annotation on functions that don't have one, and it will provide one for you to copy and paste in.
Also, some editor integrations, such as the Visual Studio Code language extension for Elm, will display these kinds of warnings as a hint icon that you can click to add the missing type annotation automatically. You can set a keyboard shortcut for this to do it without your hands leaving the keyboard.

FAKE build a project with unsafe flag

I am trying to build a solution where one of the projects needs to be build with the unsafe flag on, it is set correctly in the project however when building I get the error:
"Unsafe code may only appear if compiling with /unsafe"
This is my target at the moment
Target "CompileApp" (fun _ ->
!! #"**\*.csproj"
|> MSBuildRelease buildDir "Build"
|> Log "AppBuild-Output: "
)
I tried adding MsBuildParams but not sure how to use them yet (ie there doesnt seem to be an option in MsBuildRelease to add something like this
let setParams defaults =
{ defaults with
Verbosity = Some(Quiet)
Targets = ["Build"]
Properties =
[
"AllowUnsafeBlocks", "True"
"Configuration", "Release"
]
}
Also would the best option here be create two different targets for projects with safe and unsafe code, of would there be a better way?
I found that the AllowUnsafeBlocks=true element was only defined under the DEBUG|AnyCPU and Release|AnyCPU PropertyGroups in my project file.
Using this fixed it for me:
Target "BuildApp" (fun _ ->
!! ".\**\MyApp.*.csproj"
|> MSBuild buildDir "Build" ["Configuration", "Release"; "Platform", "AnyCPU"]
|> Log "AppBuild-Output: "
)
Hope this helps.
Ok I think this might be the way:
Target "CompileUnsafe" (fun _ ->
let buildMode = getBuildParamOrDefault "buildMode" "Release"
let setParams defaults =
{ defaults with
Verbosity = Some(Quiet)
Targets = ["Build"]
Properties =
[
"Optimize", "True"
"DebugSymbols", "True"
"Configuration", buildMode
"AllowUnsafeBlocks", "True"
]
}
build setParams "./ProjectPlugins.sln"
)
IF there are better solutions I'm all ears (the solution was there in the docs and I just missed it)