Programmatically alter Elixir Code - metaprogramming

I need to load, alter and write the code in a mix.exs file. I want to be able to load the file, write the dependencies and write the file.
I start with:
defmodule Elixir_2ndTest.Mixfile do
use Mix.Project
def project do
[app: :elixir_2nd_test,
version: "0.0.1",
elixir: "~> 1.2",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
description: description(),
deps: deps]
end
def application do
[applications: [:logger]]
end
defp deps do
[]
end
end
And I need to end up with (the only difference is in the deps fun):
defmodule Elixir_2ndTest.Mixfile do
use Mix.Project
def project do
[app: :elixir_2nd_test,
version: "0.0.1",
elixir: "~> 1.2",
build_embedded: Mix.env == :prod,
start_permanent: Mix.env == :prod,
description: description(),
deps: deps]
end
def application do
[applications: [:logger]]
end
defp deps do
[{:httpoison, "~> 0.8.3"}]
end
end
The dependencies come from a different build system (I cannot use hex directly form the public internet, so I use it in OFFLINE mode and drop the dependencies in .hex/
I know what teh depenencies and what the versions are an need to insert them in the deps function (in this case httpoison 0.8.3).
If my understanding is correct this should be possible by loading the file, quoting, altering, unquoting.
This is what I have up until this point:
{:ok, body} = File.read("mix.exs")
{:ok, ast} = Code.string_to_quoted(body)
Any pointer on how I can alter the ast and write it back would be appreciated.

It won't look exactly the same, but you can use Macro.to_string to convert the ast back to elixir code.
I was playing around with using my library PhStTransform to modify the ast and convert it back to code. Here's a very simple example from the PhStTransform test library.
test "transform quote do output" do
data = quote do: Enum.map(1..3, fn(x) -> x*x end)
data_transform = quote do: Enum.map(1..3, fn(y) -> y*y end)
replace_x = fn(a, _d ) ->
case a do
:x -> :y
atom -> atom
end
end
potion = %{ Atom => replace_x }
assert PhStTransform.transform(data, potion) == data_transform
end
What that does is convert all references to :x in the ast into :y. You'd need to be a bit more clever with writing the potion for PhStTransform, but I think it should be possible. PhStTransform is in hex.pm.
https://hex.pm/packages/phst_transform

I'm not an Elixir expert, but I know about transforming source code; see my bio.
If you have access to the AST as a data structure, you can always write procedural code to climb over it and hack at where you want something different. I assume if Elixir will give you the AST, it will give you access/modification procedures for working with it. This is compiler 101.
That's usually NOT pretty code to write or maintain. And, it may not be enough: you often need more than just the AST to do serious analysis and transformation. See my essay on Life After Parsingl. Think of this as compiler 102.
One the first stumbling blocks is regenerating text from the AST. Here is my SO discussion on how to prettyprint an AST, and why it is harder than it looks: https://stackoverflow.com/a/5834775/120163
(Sounds like Fred the Magic Wonder Dog didn't think what Elixir offered was enough and is inventing his own extensions to make this easier.).

Related

How to have custom enconding for struct using Jason?

Background
I am trying to encode a structure into json format using the Jason library. However, this is not working as expected.
Code
Let's assume I have this struct:
defmodule Test do
defstruct [:foo, :bar, :baz]
end
And that when using Jason.enconde(%Test{foo: 1, bar: 2, baz:3 }) I want this json to be created:
%{"foo" => 1, "banana" => 5}
Error
It is my understanding that to achieve this I need to implement the Jason.Enconder protocol in my struct:
https://hexdocs.pm/jason/Jason.Encoder.html
defmodule Test do
defstruct [:foo, :bar, :baz]
defimpl Jason.Encoder do
#impl Jason.Encoder
def encode(value, opts) do
Jason.Encode.map(%{foo: Map.get(value, :foo), banana: Map.get(value, :bar, 0) + Map.get(value, :baz, 0)}, opts)
end
end
end
However, this will not work:
Jason.encode(%Test{foo: 1, bar: 2, baz: 3})
{:error,
%Protocol.UndefinedError{
description: "Jason.Encoder protocol must always be explicitly implemented.\n\nIf you own the struct, you can derive the implementation specifying which fields should be encoded to JSON:\n\n #derive {Jason.Encoder, only: [....]}\n defstruct ...\n\nIt is also possible to encode all fields, although this should be used carefully to avoid accidentally leaking private information when new fields are added:\n\n #derive Jason.Encoder\n defstruct ...\n\nFinally, if you don't own the struct you want to encode to JSON, you may use Protocol.derive/3 placed outside of any module:\n\n Protocol.derive(Jason.Encoder, NameOfTheStruct, only: [...])\n Protocol.derive(Jason.Encoder, NameOfTheStruct)\n",
protocol: Jason.Encoder,
value: %Test{bar: 2, baz: 3, foo: 1}
}}
From what I understand, it looks like I can only select/exclude keys to serialize, I cannot transform/add new keys.
Since I own the structure in question, using Protocol.derive is not necessary.
However I fail to understand how I can leverage the Jason.Encoder protocol to achieve what I want.
Questions
Is my objective possible using the Jason library, or is this a limitation?
Am I miss understanding the documentation and doing something incorrect?
My guess is, this is due to writing the protocol inside a test file. Protocol consolidation happens before the test file executes, so the protocol never becomes part of the compiled codebase.
To elaborate with an example...
I did the following in a Phoenix app
into the lib folder, I added foo.ex
defmodule Foo do
defstruct [:a, :b]
defimpl Jason.Encoder do
def encode(%Foo{a: a, b: b}, opts) do
Jason.Encode.map(%{"a" => a, "b" => b}, opts)
end
end
end
in the test folder, I added foo_test.exs
defmodule FooTest do
use ExUnit.Case
defmodule Bar do
defstruct [:c, :d]
defimpl Jason.Encoder do
def encode(%Bar{c: c, d: d}, opts) do
Jason.Encode.map(%{"c" => c, "d" => d}, opts)
end
end
end
test "encodes Foo" do
%Foo{a: 1, b: 2} |> Jason.encode!() |> IO.inspect()
end
test "encodes Bar" do
%Bar{c: 5, d: 6} |> Jason.encode!()
end
end
Running this test fule, results in the "encodes Foo" passing, but "encodes Bar" fails with a warning
warning: the Jason.Encoder protocol has already been consolidated, an implementation for FooTest.Bar has no effect. If you want to implement protocols after compilation or during tests, check the "Consolidation" section in the Protocol module documentation
followed by an error in the test
** (Protocol.UndefinedError) protocol Jason.Encoder not implemented for %FooTest.Bar{c: 5, d: 6} of type FooTest.Bar (a struct), Jason.Encoder protocol must always be explicitly implemented.
This is because of protocol consolidation happening, causing the Bar protocol to not be compiled.
You can turn off protocol consolidation in the test environment, by adding the following to mix.exs
def project do
# ...
consolidate_protocols: Mix.env() != :test,
#...
end
If you do that, the protocol will compile and both tests will pass.
However, the solution is probably to just not write the struct/protocol directly in the test file.

Use OCaml warning attribute to disable warning 8: unexhaustive match

I'm trying to write code similar to the following:
let [a; b] =
(* body *)
[1; 2]
And I want to disable warning # 8 just for the pattern [a; b] and not for the body or for anything outside of the let. I've tried to put the warning attribute to disable the warning, but none of the below work:
let[#warning "-8"] [a[#warning "-8"];b[#warning "-8"]] [#warning "-8"] =
[1;2][#warning "-8"]
[##ocaml.warning "-8"]
P.S. I'm not really writing this code, but am experimenting with a custom PPX preprocessor. So a convoluted but working example is acceptable.
Local disabling of warnings with [#warning "…"] and [##warning "…"]is not well supported for OCaml version anterior to 4.06.0 . For such version, one possibility might be to use enclosing[###warning ""] attribute:
[###warning "-8"]
let [a;b] = [1;2]
[###warning "+8"]
but this also deactivate the warning inside the body.
If you are generating the code and know statiscally the size of the list, another option might be to use a tuple for the binding (aka let (a,b)= …)?

How to use yml file in my cucumber test?

I want to use config.yml file in my cucumber-test. I whrite such like this:
test_config.yml:
group_name: animals
learn_group_name: dogs
card_box_name: cats
cucumber_test.rb:
require `watir-webdriver`
require `yaml`
def read_config
config = YAML.load(File.read(`/home/profile/Desktop/cucumber/test_config.yml`))
#group = config[`group_name`]
#learn_group = config[`learn_group_name`]
#card_box = config[`card_box_name`]
end
puts `Group = #{#group}`
puts `Learn group = #{#learn_group}`
puts `Card box = #{#card_box}`
...
in console (terminal):
Group =
Learn group =
Card box =
...
But what is wrong?
This is a really bad code example.
You are you using backticks (`) instead of quotes ("). That will cause problems. A lot of problems.
The problem is not related to cucumber or watir-webdriver gems. You are defining instance variables (#group...) in a method (read_config) but then you are never calling the method.
Please update the question with more realistic example.
Before do
#host ||= YML["host"]
#group_name_for_search ||= YML["group_name_for_search"]
#learn_group_name_for_search ||= YML["learn_group_name_for_search"]
#card_box_name_for_search ||= YML['card_box_name_for_search']
end
I must to define, before the cucumber steps, Before do.

Quick Check for CoffeeScript

Does it exist? I can't find it and it isn't listed on wikipedia. (which means it doesn't exist :) )
I know node.js has it. Not sure if writing my node app in coffeescript and applying quick check would work.
http://en.wikipedia.org/wiki/Quick_check
Any clues?
I don't know of any QuickCheck library written particularly in or for CoffeeScript, but googling pulls up qc.js. Here's a snippet from demo.js in that repository:
declare("reverse", [arbWholeNumList, arbWholeNumList],
function(c, x, y) {
var z = x.concat(y);
x.reverse();
y.reverse();
z.reverse();
c.assert(z.toString() == y.concat(x).toString());
});
Now I'm no CoffeeScript expert, but I ran this through http://js2coffee.org. If you can manage to import qc.js, then using it from CoffeeScript would look something like this:
declare "reverse", [ arbWholeNumList, arbWholeNumList ], (c, x, y) ->
z = x.concat(y)
x.reverse()
y.reverse()
z.reverse()
c.assert z.toString() is y.concat(x).toString()

How can I add the build version to a scons build

At the moment I'm using some magic to get the current git revision into my scons builds.. I just grab the version a stick it into CPPDEFINES.
It works quite nicely ... until the version changes and scons wants to rebuild everything, rather than just the files that have changed - becasue the define that all files use has changed.
Ideally I'd generate a file using a custom builder called git_version.cpp and
just have a function in there that returns the right tag. That way only that one file would be rebuilt.
Now I'm sure I've seen a tutorial showing exactly how to do this .. but I can't seem to track it down. And I find the custom builder stuff a little odd in scons...
So any pointers would be appreciated...
Anyway just for reference this is what I'm currently doing:
# Lets get the version from git
# first get the base version
git_sha = subprocess.Popen(["git","rev-parse","--short=10","HEAD"], stdout=subprocess.PIPE ).communicate()[0].strip()
p1 = subprocess.Popen(["git", "status"], stdout=subprocess.PIPE )
p2 = subprocess.Popen(["grep", "Changed but not updated\\|Changes to be committed"], stdin=p1.stdout,stdout=subprocess.PIPE)
result = p2.communicate()[0].strip()
if result!="":
git_sha += "[MOD]"
print "Building version %s"%git_sha
env = Environment()
env.Append( CPPDEFINES={'GITSHAMOD':'"\\"%s\\""'%git_sha} )
You don't need a custom Builder since this is just one file. You can use a function (attached to the target version file as an Action) to generate your version file. In the example code below, I've already computed the version and put it into an environment variable. You could do the same, or you could put your code that makes git calls in the version_action function.
version_build_template="""/*
* This file is automatically generated by the build process
* DO NOT EDIT!
*/
const char VERSION_STRING[] = "%s";
const char* getVersionString() { return VERSION_STRING; }
"""
def version_action(target, source, env):
"""
Generate the version file with the current version in it
"""
contents = version_build_template % (env['VERSION'].toString())
fd = open(target[0].path, 'w')
fd.write(contents)
fd.close()
return 0
build_version = env.Command('version.build.cpp', [], Action(version_action))
env.AlwaysBuild(build_version)

Resources